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", ] + 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..9dd116fc70a 100644 --- a/apiserver/plane/api/views/cycle.py +++ b/apiserver/plane/api/views/cycle.py @@ -661,61 +661,74 @@ def post(self, request, slug, project_id, cycle_id): workspace__slug=slug, project_id=project_id, pk=cycle_id ) - 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 +736,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, diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index 68ffb1aeece..af663aa1cd9 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -151,6 +151,25 @@ 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: + 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( @@ -315,8 +334,11 @@ 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.save(update_fields=["created_at"]) + issue.created_at = request.data.get("created_at", timezone.now()) + issue.created_by_id = request.data.get( + "created_by", request.user.id + ) + issue.save(update_fields=["created_at", "created_by"]) # Track the issue issue_activity.delay( @@ -610,14 +632,20 @@ def post(self, request, slug, project_id, issue_id): project_id=project_id, issue_id=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=str(self.request.user.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()), ) @@ -771,12 +799,24 @@ 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", timezone.now() + ) + 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( 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, diff --git a/apiserver/plane/api/views/member.py b/apiserver/plane/api/views/member.py index 5d47bbb0684..08bbd9a4d06 100644 --- a/apiserver/plane/api/views/member.py +++ b/apiserver/plane/api/views/member.py @@ -21,11 +21,19 @@ ProjectMember, ) +from plane.app.permissions import ( + ProjectMemberPermission, +) + # API endpoint to get and insert users inside the workspace -class WorkspaceMemberAPIEndpoint(BaseAPIView): +class ProjectMemberAPIEndpoint(BaseAPIView): + permission_classes = [ + ProjectMemberPermission, + ] + # 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( @@ -34,14 +42,14 @@ def get(self, request, slug): ) # 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__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=workspace_members.values_list("member_id", flat=True) + id__in=project_members, ), many=True, ).data @@ -49,14 +57,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 +83,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 +150,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 +