From b3d29df8ce3caf3dab7592ccf8cfa4a2b5b88e22 Mon Sep 17 00:00:00 2001 From: Henit Chobisa Date: Mon, 29 Jul 2024 19:39:44 +0530 Subject: [PATCH 1/8] feat: removed created by and created_at as readonly fields from issue serializers --- apiserver/plane/api/serializers/issue.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apiserver/plane/api/serializers/issue.py b/apiserver/plane/api/serializers/issue.py index e60b3a1374a..fd89a3e05e0 100644 --- a/apiserver/plane/api/serializers/issue.py +++ b/apiserver/plane/api/serializers/issue.py @@ -53,7 +53,6 @@ class Meta: "id", "workspace", "project", - "created_by", "updated_by", "updated_at", ] @@ -338,9 +337,7 @@ class Meta: "workspace", "project", "issue", - "created_by", "updated_by", - "created_at", "updated_at", ] @@ -433,3 +430,4 @@ class Meta: "created_at", "updated_at", ] + From 50aee23b1442154681cd580730d3f3ad839d0c7d Mon Sep 17 00:00:00 2001 From: Henit Chobisa Date: Mon, 29 Jul 2024 19:54:53 +0530 Subject: [PATCH 2/8] feat: modified serializers for accepting created_by, and changed workspacememberendpoint to projectmemberendpoint --- apiserver/plane/api/urls/member.py | 6 +- apiserver/plane/api/views/__init__.py | 3 +- apiserver/plane/api/views/cycle.py | 110 ++++++++++-------- apiserver/plane/api/views/issue.py | 55 ++++++++- apiserver/plane/api/views/member.py | 20 ++-- .../plane/bgtasks/issue_activites_task.py | 4 +- 6 files changed, 132 insertions(+), 66 deletions(-) diff --git a/apiserver/plane/api/urls/member.py b/apiserver/plane/api/urls/member.py index 9a622d35a67..5fe1785a7e2 100644 --- a/apiserver/plane/api/urls/member.py +++ b/apiserver/plane/api/urls/member.py @@ -1,13 +1,13 @@ from django.urls import path from plane.api.views import ( - WorkspaceMemberAPIEndpoint, + ProjectMemberAPIEndpoint, ) urlpatterns = [ path( - "workspaces//members/", - WorkspaceMemberAPIEndpoint.as_view(), + "workspaces//projects//members/", + ProjectMemberAPIEndpoint.as_view(), name="users", ), ] diff --git a/apiserver/plane/api/views/__init__.py b/apiserver/plane/api/views/__init__.py index 48461cee273..bbec428c053 100644 --- a/apiserver/plane/api/views/__init__.py +++ b/apiserver/plane/api/views/__init__.py @@ -25,6 +25,7 @@ ModuleArchiveUnarchiveAPIEndpoint, ) -from .member import WorkspaceMemberAPIEndpoint +from .member import ProjectMemberAPIEndpoint from .inbox import InboxIssueAPIEndpoint + diff --git a/apiserver/plane/api/views/cycle.py b/apiserver/plane/api/views/cycle.py index 5a11c30dca2..cb7ab352d3a 100644 --- a/apiserver/plane/api/views/cycle.py +++ b/apiserver/plane/api/views/cycle.py @@ -26,7 +26,7 @@ CycleSerializer, ) from plane.app.permissions import ProjectEntityPermission -from plane.bgtasks.issue_activites_task import issue_activity +from plane.bgtasks.issue_activities_task import issue_activity from plane.db.models import ( Cycle, CycleIssue, @@ -660,62 +660,76 @@ def post(self, request, slug, project_id, cycle_id): cycle = Cycle.objects.get( workspace__slug=slug, project_id=project_id, pk=cycle_id ) + print("cycle name", cycle.name) - issues = Issue.objects.filter( - pk__in=issues, workspace__slug=slug, project_id=project_id - ).values_list("id", flat=True) + if ( + cycle.end_date is not None + and cycle.end_date < timezone.now().date() + ): + return Response( + { + "error": "The Cycle has already been completed so no new issues can be added" + }, + status=status.HTTP_400_BAD_REQUEST, + ) # Get all CycleIssues already created - cycle_issues = list(CycleIssue.objects.filter(issue_id__in=issues)) - update_cycle_issue_activity = [] - record_to_create = [] - records_to_update = [] - - for issue in issues: - cycle_issue = [ - cycle_issue - for cycle_issue in cycle_issues - if str(cycle_issue.issue_id) in issues - ] - # Update only when cycle changes - if len(cycle_issue): - if cycle_issue[0].cycle_id != cycle_id: - update_cycle_issue_activity.append( - { - "old_cycle_id": str(cycle_issue[0].cycle_id), - "new_cycle_id": str(cycle_id), - "issue_id": str(cycle_issue[0].issue_id), - } - ) - cycle_issue[0].cycle_id = cycle_id - records_to_update.append(cycle_issue[0]) - else: - record_to_create.append( - CycleIssue( - project_id=project_id, - workspace=cycle.workspace, - created_by=request.user, - updated_by=request.user, - cycle=cycle, - issue_id=issue, - ) - ) + cycle_issues = list( + CycleIssue.objects.filter( + ~Q(cycle_id=cycle_id), issue_id__in=issues + ) + ) - CycleIssue.objects.bulk_create( - record_to_create, - batch_size=10, + existing_issues = [ + str(cycle_issue.issue_id) + for cycle_issue in cycle_issues + if str(cycle_issue.issue_id) in issues + ] + new_issues = list(set(issues) - set(existing_issues)) + + # New issues to create + created_records = CycleIssue.objects.bulk_create( + [ + CycleIssue( + project_id=project_id, + workspace_id=cycle.workspace_id, + cycle_id=cycle_id, + issue_id=issue, + ) + for issue in new_issues + ], ignore_conflicts=True, + batch_size=10, ) + + # Updated Issues + updated_records = [] + update_cycle_issue_activity = [] + # Iterate over each cycle_issue in cycle_issues + for cycle_issue in cycle_issues: + old_cycle_id = cycle_issue.cycle_id + # Update the cycle_issue's cycle_id + cycle_issue.cycle_id = cycle_id + # Add the modified cycle_issue to the records_to_update list + updated_records.append(cycle_issue) + # Record the update activity + update_cycle_issue_activity.append( + { + "old_cycle_id": str(old_cycle_id), + "new_cycle_id": str(cycle_id), + "issue_id": str(cycle_issue.issue_id), + } + ) + + # Update the cycle issues CycleIssue.objects.bulk_update( - records_to_update, - ["cycle"], - batch_size=10, + updated_records, ["cycle_id"], batch_size=100 ) # Capture Issue Activity issue_activity.delay( type="cycle.activity.created", - requested_data=json.dumps({"cycles_list": str(issues)}), + requested_data=json.dumps({"cycles_list": issues}), actor_id=str(self.request.user.id), issue_id=None, project_id=str(self.kwargs.get("project_id", None)), @@ -723,13 +737,14 @@ def post(self, request, slug, project_id, cycle_id): { "updated_cycle_issues": update_cycle_issue_activity, "created_cycle_issues": serializers.serialize( - "json", record_to_create + "json", created_records ), } ), epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), ) - # Return all Cycle Issues return Response( CycleIssueSerializer(self.get_queryset(), many=True).data, @@ -1178,3 +1193,4 @@ def post(self, request, slug, project_id, cycle_id): ) return Response({"message": "Success"}, status=status.HTTP_200_OK) + diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index 68ffb1aeece..0d0f36ed50c 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -4,6 +4,7 @@ from django.core.serializers.json import DjangoJSONEncoder # Django imports +from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned from django.db import IntegrityError from django.db.models import ( Case, @@ -38,7 +39,7 @@ ProjectLitePermission, ProjectMemberPermission, ) -from plane.bgtasks.issue_activites_task import issue_activity +from plane.bgtasks.issue_activities_task import issue_activity from plane.db.models import ( Issue, IssueActivity, @@ -151,6 +152,36 @@ def get_queryset(self): ).distinct() def get(self, request, slug, project_id, pk=None): + external_id = request.GET.get("external_id") + external_source = request.GET.get("external_source") + + if external_id and external_source: + try: + issue = Issue.objects.get( + external_id=external_id, + external_source=external_source, + workspace__slug=slug, + project_id=project_id, + ) + return Response( + IssueSerializer( + issue, + fields=self.fields, + expand=self.expand, + ).data, + status=status.HTTP_200_OK, + ) + except ObjectDoesNotExist: + return Response( + {"detail": "Issue not found."}, + status=status.HTTP_404_NOT_FOUND, + ) + except MultipleObjectsReturned: + return Response( + {"detail": "Multiple issues found."}, + status=status.HTTP_400_BAD_REQUEST, + ) + if pk: issue = Issue.issue_objects.annotate( sub_issues_count=Issue.issue_objects.filter( @@ -316,7 +347,8 @@ def post(self, request, slug, project_id): pk=serializer.data["id"], ).first() issue.created_at = request.data.get("created_at") - issue.save(update_fields=["created_at"]) + issue.created_by_id = request.data.get("created_by") + issue.save(update_fields=["created_at", "created_by"]) # Track the issue issue_activity.delay( @@ -610,12 +642,18 @@ def post(self, request, slug, project_id, issue_id): project_id=project_id, issue_id=issue_id, ) + + # Get the issue for fetching the created_by id + issue = Issue.objects.get( + workspace__slug=slug, project_id=project_id, pk=issue_id + ) + issue_activity.delay( type="link.activity.created", requested_data=json.dumps( serializer.data, cls=DjangoJSONEncoder ), - actor_id=str(self.request.user.id), + actor_id=issue.created_by_id, issue_id=str(self.kwargs.get("issue_id")), project_id=str(self.kwargs.get("project_id")), current_instance=None, @@ -771,12 +809,20 @@ def post(self, request, slug, project_id, issue_id): issue_id=issue_id, actor=request.user, ) + issue_comment = IssueComment.objects.get( + pk=serializer.data.get("id") + ) + # Update the created_at and the created_by and save the comment + issue_comment.created_at = request.data.get("created_at") + issue_comment.created_by_id = request.data.get("created_by") + issue_comment.save(update_fields=["created_at", "created_by"]) + issue_activity.delay( type="comment.activity.created", requested_data=json.dumps( serializer.data, cls=DjangoJSONEncoder ), - actor_id=str(self.request.user.id), + actor_id=str(issue_comment.created_by_id), issue_id=str(self.kwargs.get("issue_id")), project_id=str(self.kwargs.get("project_id")), current_instance=None, @@ -977,3 +1023,4 @@ def get(self, request, slug, project_id, issue_id): ) serializer = IssueAttachmentSerializer(issue_attachments, many=True) return Response(serializer.data, status=status.HTTP_200_OK) + diff --git a/apiserver/plane/api/views/member.py b/apiserver/plane/api/views/member.py index 5d47bbb0684..c63ddea9e58 100644 --- a/apiserver/plane/api/views/member.py +++ b/apiserver/plane/api/views/member.py @@ -23,9 +23,9 @@ # API endpoint to get and insert users inside the workspace -class WorkspaceMemberAPIEndpoint(BaseAPIView): +class ProjectMemberAPIEndpoint(BaseAPIView): # Get all the users that are present inside the workspace - def get(self, request, slug): + def get(self, request, slug, project_id): # Check if the workspace exists if not Workspace.objects.filter(slug=slug).exists(): return Response( @@ -33,15 +33,17 @@ def get(self, request, slug): status=status.HTTP_400_BAD_REQUEST, ) + workspace = Workspace.objects.filter(slug=slug).first() + # Get the workspace members that are present inside the workspace - workspace_members = WorkspaceMember.objects.filter( - workspace__slug=slug + project_members = ProjectMember.objects.filter( + project_id=project_id, workspace_id=workspace.id ) # Get all the users that are present inside the workspace users = UserLiteSerializer( User.objects.filter( - id__in=workspace_members.values_list("member_id", flat=True) + id__in=project_members.values_list("member_id", flat=True) ), many=True, ).data @@ -49,14 +51,13 @@ def get(self, request, slug): return Response(users, status=status.HTTP_200_OK) # Insert a new user inside the workspace, and assign the user to the project - def post(self, request, slug): + def post(self, request, slug, project_id): # Check if user with email already exists, and send bad request if it's # not present, check for workspace and valid project mandat # ------------------- Validation ------------------- if ( request.data.get("email") is None or request.data.get("display_name") is None - or request.data.get("project_id") is None ): return Response( { @@ -76,9 +77,7 @@ def post(self, request, slug): ) workspace = Workspace.objects.filter(slug=slug).first() - project = Project.objects.filter( - pk=request.data.get("project_id") - ).first() + project = Project.objects.filter(pk=project_id).first() if not all([workspace, project]): return Response( @@ -145,3 +144,4 @@ def post(self, request, slug): user_data = UserLiteSerializer(user).data return Response(user_data, status=status.HTTP_201_CREATED) + diff --git a/apiserver/plane/bgtasks/issue_activites_task.py b/apiserver/plane/bgtasks/issue_activites_task.py index 0c2af603953..cbc8be470b6 100644 --- a/apiserver/plane/bgtasks/issue_activites_task.py +++ b/apiserver/plane/bgtasks/issue_activites_task.py @@ -593,7 +593,8 @@ def create_issue_activity( epoch=epoch, ) issue_activity.created_at = issue.created_at - issue_activity.save(update_fields=["created_at"]) + issue_activity.actor_id = issue.created_by_id + issue_activity.save(update_fields=["created_at", "actor_id"]) requested_data = ( json.loads(requested_data) if requested_data is not None else None ) @@ -1773,3 +1774,4 @@ def issue_activity( except Exception as e: log_exception(e) return + From dcec0e223ffb0d98cc524da96002036efb80db9c Mon Sep 17 00:00:00 2001 From: Henit Chobisa Date: Mon, 29 Jul 2024 20:27:43 +0530 Subject: [PATCH 3/8] fix: code suggestions --- apiserver/plane/api/views/cycle.py | 1 - apiserver/plane/api/views/issue.py | 40 +++++++++++------------------- 2 files changed, 14 insertions(+), 27 deletions(-) diff --git a/apiserver/plane/api/views/cycle.py b/apiserver/plane/api/views/cycle.py index cb7ab352d3a..bf2f982bb0e 100644 --- a/apiserver/plane/api/views/cycle.py +++ b/apiserver/plane/api/views/cycle.py @@ -660,7 +660,6 @@ def post(self, request, slug, project_id, cycle_id): cycle = Cycle.objects.get( workspace__slug=slug, project_id=project_id, pk=cycle_id ) - print("cycle name", cycle.name) if ( cycle.end_date is not None diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index 0d0f36ed50c..5efa076bf3f 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -156,32 +156,20 @@ def get(self, request, slug, project_id, pk=None): external_source = request.GET.get("external_source") if external_id and external_source: - try: - issue = Issue.objects.get( - external_id=external_id, - external_source=external_source, - workspace__slug=slug, - project_id=project_id, - ) - return Response( - IssueSerializer( - issue, - fields=self.fields, - expand=self.expand, - ).data, - status=status.HTTP_200_OK, - ) - except ObjectDoesNotExist: - return Response( - {"detail": "Issue not found."}, - status=status.HTTP_404_NOT_FOUND, - ) - except MultipleObjectsReturned: - return Response( - {"detail": "Multiple issues found."}, - status=status.HTTP_400_BAD_REQUEST, - ) - + issue = Issue.objects.get( + external_id=external_id, + external_source=external_source, + workspace__slug=slug, + project_id=project_id, + ) + return Response( + IssueSerializer( + issue, + fields=self.fields, + expand=self.expand, + ).data, + status=status.HTTP_200_OK, + ) if pk: issue = Issue.issue_objects.annotate( sub_issues_count=Issue.issue_objects.filter( From 3a08261baabac165228d5ff0bbd4f0238c5cb76c Mon Sep 17 00:00:00 2001 From: Henit Chobisa Date: Tue, 30 Jul 2024 12:09:38 +0530 Subject: [PATCH 4/8] chore: resolved code review --- apiserver/plane/api/views/member.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/apiserver/plane/api/views/member.py b/apiserver/plane/api/views/member.py index c63ddea9e58..cf17756ab68 100644 --- a/apiserver/plane/api/views/member.py +++ b/apiserver/plane/api/views/member.py @@ -33,17 +33,15 @@ def get(self, request, slug, project_id): status=status.HTTP_400_BAD_REQUEST, ) - workspace = Workspace.objects.filter(slug=slug).first() - # Get the workspace members that are present inside the workspace project_members = ProjectMember.objects.filter( - project_id=project_id, workspace_id=workspace.id - ) + project_id=project_id, workspace__slug=slug + ).values_list("member_id", flat=True) # Get all the users that are present inside the workspace users = UserLiteSerializer( User.objects.filter( - id__in=project_members.values_list("member_id", flat=True) + id__in=project_members, ), many=True, ).data From a033e55f85fe5bacb5ea9a6bd7e22cde43381674 Mon Sep 17 00:00:00 2001 From: Henit Chobisa Date: Tue, 30 Jul 2024 12:10:17 +0530 Subject: [PATCH 5/8] chore: removed unused imports --- apiserver/plane/api/views/issue.py | 1 - 1 file changed, 1 deletion(-) diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index 5efa076bf3f..ed473a9d0e1 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -4,7 +4,6 @@ from django.core.serializers.json import DjangoJSONEncoder # Django imports -from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned from django.db import IntegrityError from django.db.models import ( Case, From a8dfaa46721422ca7857ebcd5c58782810959ca4 Mon Sep 17 00:00:00 2001 From: Henit Chobisa Date: Tue, 30 Jul 2024 12:27:50 +0530 Subject: [PATCH 6/8] fix: passed default user if created_by is absent, and permission classes --- apiserver/plane/api/views/issue.py | 19 ++++++++++++------- apiserver/plane/api/views/member.py | 8 ++++++++ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index ed473a9d0e1..0f103ff0196 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -169,6 +169,7 @@ def get(self, request, slug, project_id, pk=None): ).data, status=status.HTTP_200_OK, ) + if pk: issue = Issue.issue_objects.annotate( sub_issues_count=Issue.issue_objects.filter( @@ -334,7 +335,9 @@ def post(self, request, slug, project_id): pk=serializer.data["id"], ).first() issue.created_at = request.data.get("created_at") - issue.created_by_id = request.data.get("created_by") + issue.created_by_id = request.data.get( + "created_by", request.user.id + ) issue.save(update_fields=["created_at", "created_by"]) # Track the issue @@ -630,19 +633,19 @@ def post(self, request, slug, project_id, issue_id): issue_id=issue_id, ) - # Get the issue for fetching the created_by id - issue = Issue.objects.get( - workspace__slug=slug, project_id=project_id, pk=issue_id + link = IssueLink.objects.get(pk=serializer.data["id"]) + link.created_by_id = request.data.get( + "created_by", request.user.id ) - + link.save(update_fields=["created_by"]) issue_activity.delay( type="link.activity.created", requested_data=json.dumps( serializer.data, cls=DjangoJSONEncoder ), - actor_id=issue.created_by_id, issue_id=str(self.kwargs.get("issue_id")), project_id=str(self.kwargs.get("project_id")), + actor_id=str(link.created_by_id), current_instance=None, epoch=int(timezone.now().timestamp()), ) @@ -801,7 +804,9 @@ def post(self, request, slug, project_id, issue_id): ) # Update the created_at and the created_by and save the comment issue_comment.created_at = request.data.get("created_at") - issue_comment.created_by_id = request.data.get("created_by") + issue_comment.created_by_id = request.data.get( + "created_by", request.user.id + ) issue_comment.save(update_fields=["created_at", "created_by"]) issue_activity.delay( diff --git a/apiserver/plane/api/views/member.py b/apiserver/plane/api/views/member.py index cf17756ab68..08bbd9a4d06 100644 --- a/apiserver/plane/api/views/member.py +++ b/apiserver/plane/api/views/member.py @@ -21,9 +21,17 @@ ProjectMember, ) +from plane.app.permissions import ( + ProjectMemberPermission, +) + # API endpoint to get and insert users inside the workspace class ProjectMemberAPIEndpoint(BaseAPIView): + permission_classes = [ + ProjectMemberPermission, + ] + # Get all the users that are present inside the workspace def get(self, request, slug, project_id): # Check if the workspace exists From 3d913cd6bf5e3b37f92756d91c82b2cb3fd71759 Mon Sep 17 00:00:00 2001 From: Henit Chobisa Date: Tue, 30 Jul 2024 12:42:42 +0530 Subject: [PATCH 7/8] fix: default value for the issue creation --- apiserver/plane/api/views/issue.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index 0f103ff0196..c72ff515439 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -334,7 +334,7 @@ def post(self, request, slug, project_id): project_id=project_id, pk=serializer.data["id"], ).first() - issue.created_at = request.data.get("created_at") + issue.created_at = request.data.get("created_at", timezone.now()) issue.created_by_id = request.data.get( "created_by", request.user.id ) @@ -803,7 +803,9 @@ def post(self, request, slug, project_id, issue_id): pk=serializer.data.get("id") ) # Update the created_at and the created_by and save the comment - issue_comment.created_at = request.data.get("created_at") + issue_comment.created_at = request.data.get( + "created_at", timezone.now() + ) issue_comment.created_by_id = request.data.get( "created_by", request.user.id ) From a0c21b38267fcde2bf8cb597e7964e46e7add7f1 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Tue, 30 Jul 2024 12:45:14 +0530 Subject: [PATCH 8/8] dev: fix nomenclature --- apiserver/plane/api/views/cycle.py | 3 +-- apiserver/plane/api/views/issue.py | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/apiserver/plane/api/views/cycle.py b/apiserver/plane/api/views/cycle.py index bf2f982bb0e..9dd116fc70a 100644 --- a/apiserver/plane/api/views/cycle.py +++ b/apiserver/plane/api/views/cycle.py @@ -26,7 +26,7 @@ CycleSerializer, ) from plane.app.permissions import ProjectEntityPermission -from plane.bgtasks.issue_activities_task import issue_activity +from plane.bgtasks.issue_activites_task import issue_activity from plane.db.models import ( Cycle, CycleIssue, @@ -1192,4 +1192,3 @@ def post(self, request, slug, project_id, cycle_id): ) return Response({"message": "Success"}, status=status.HTTP_200_OK) - diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index c72ff515439..af663aa1cd9 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -38,7 +38,7 @@ ProjectLitePermission, ProjectMemberPermission, ) -from plane.bgtasks.issue_activities_task import issue_activity +from plane.bgtasks.issue_activites_task import issue_activity from plane.db.models import ( Issue, IssueActivity, @@ -1017,4 +1017,3 @@ def get(self, request, slug, project_id, issue_id): ) serializer = IssueAttachmentSerializer(issue_attachments, many=True) return Response(serializer.data, status=status.HTTP_200_OK) -