diff --git a/apiserver/plane/api/views/cycle.py b/apiserver/plane/api/views/cycle.py index be23256ac6d..48e7f6d1f67 100644 --- a/apiserver/plane/api/views/cycle.py +++ b/apiserver/plane/api/views/cycle.py @@ -207,8 +207,7 @@ def get(self, request, slug, project_id, pk=None): # Incomplete Cycles if cycle_view == "incomplete": queryset = queryset.filter( - Q(end_date__gte=timezone.now()) - | Q(end_date__isnull=True), + Q(end_date__gte=timezone.now()) | Q(end_date__isnull=True), ) return self.paginate( request=request, @@ -309,10 +308,7 @@ def patch(self, request, slug, project_id, pk): request_data = request.data - if ( - cycle.end_date is not None - and cycle.end_date < timezone.now() - ): + if cycle.end_date is not None and cycle.end_date < timezone.now(): if "sort_order" in request_data: # Can only change sort order request_data = { diff --git a/apiserver/plane/app/serializers/draft.py b/apiserver/plane/app/serializers/draft.py index 1ca9a743128..2128b927d22 100644 --- a/apiserver/plane/app/serializers/draft.py +++ b/apiserver/plane/app/serializers/draft.py @@ -49,7 +49,6 @@ class Meta: fields = "__all__" read_only_fields = [ "workspace", - "project", "created_by", "updated_by", "created_at", @@ -83,11 +82,13 @@ def create(self, validated_data): modules = self.initial_data.get("module_ids", None) workspace_id = self.context["workspace_id"] + project_id = self.context["project_id"] # Create Issue issue = DraftIssue.objects.create( **validated_data, workspace_id=workspace_id, + project_id=project_id, ) # Issue Audit Users @@ -99,8 +100,9 @@ def create(self, validated_data): [ DraftIssueAssignee( assignee=user, - issue=issue, + draft_issue=issue, workspace_id=workspace_id, + project_id=project_id, created_by_id=created_by_id, updated_by_id=updated_by_id, ) @@ -114,7 +116,8 @@ def create(self, validated_data): [ DraftIssueLabel( label=label, - issue=issue, + draft_issue=issue, + project_id=project_id, workspace_id=workspace_id, created_by_id=created_by_id, updated_by_id=updated_by_id, @@ -128,6 +131,7 @@ def create(self, validated_data): DraftIssueCycle.objects.create( cycle_id=cycle_id, draft_issue=issue, + project_id=project_id, workspace_id=workspace_id, created_by_id=created_by_id, updated_by_id=updated_by_id, @@ -139,6 +143,7 @@ def create(self, validated_data): DraftIssueModule( module_id=module_id, draft_issue=issue, + project_id=project_id, workspace_id=workspace_id, created_by_id=created_by_id, updated_by_id=updated_by_id, @@ -153,22 +158,25 @@ def create(self, validated_data): def update(self, instance, validated_data): assignees = validated_data.pop("assignee_ids", None) labels = validated_data.pop("label_ids", None) - cycle_id = self.initial_data.get("cycle_id", None) + cycle_id = self.context.get("cycle_id", None) modules = self.initial_data.get("module_ids", None) # Related models workspace_id = instance.workspace_id + project_id = instance.project_id + created_by_id = instance.created_by_id updated_by_id = instance.updated_by_id if assignees is not None: - DraftIssueAssignee.objects.filter(issue=instance).delete() + DraftIssueAssignee.objects.filter(draft_issue=instance).delete() DraftIssueAssignee.objects.bulk_create( [ DraftIssueAssignee( assignee=user, - issue=instance, + draft_issue=instance, workspace_id=workspace_id, + project_id=project_id, created_by_id=created_by_id, updated_by_id=updated_by_id, ) @@ -178,13 +186,14 @@ def update(self, instance, validated_data): ) if labels is not None: - DraftIssueLabel.objects.filter(issue=instance).delete() + DraftIssueLabel.objects.filter(draft_issue=instance).delete() DraftIssueLabel.objects.bulk_create( [ DraftIssueLabel( label=label, - issue=instance, + draft_issue=instance, workspace_id=workspace_id, + project_id=project_id, created_by_id=created_by_id, updated_by_id=updated_by_id, ) @@ -193,28 +202,31 @@ def update(self, instance, validated_data): batch_size=10, ) - if cycle_id is not None: + if cycle_id != "not_provided": DraftIssueCycle.objects.filter(draft_issue=instance).delete() - DraftIssueCycle.objects.create( - cycle_id=cycle_id, - draft_issue=instance, - workspace_id=workspace_id, - created_by_id=created_by_id, - updated_by_id=updated_by_id, - ) + if cycle_id is not None: + DraftIssueCycle.objects.create( + cycle_id=cycle_id, + draft_issue=instance, + workspace_id=workspace_id, + project_id=project_id, + created_by_id=created_by_id, + updated_by_id=updated_by_id, + ) if modules is not None: DraftIssueModule.objects.filter(draft_issue=instance).delete() DraftIssueModule.objects.bulk_create( [ DraftIssueModule( - module=module, + module_id=module_id, draft_issue=instance, workspace_id=workspace_id, + project_id=project_id, created_by_id=created_by_id, updated_by_id=updated_by_id, ) - for module in modules + for module_id in modules ], batch_size=10, ) diff --git a/apiserver/plane/app/urls/workspace.py b/apiserver/plane/app/urls/workspace.py index 7dab175441c..fb6f4c13acc 100644 --- a/apiserver/plane/app/urls/workspace.py +++ b/apiserver/plane/app/urls/workspace.py @@ -276,4 +276,9 @@ ), name="workspace-drafts-issues", ), + path( + "workspaces//draft-to-issue//", + WorkspaceDraftIssueViewSet.as_view({"post": "create_draft_to_issue"}), + name="workspace-drafts-issues", + ), ] diff --git a/apiserver/plane/app/views/cycle/base.py b/apiserver/plane/app/views/cycle/base.py index df1bd93feba..3a372d36ded 100644 --- a/apiserver/plane/app/views/cycle/base.py +++ b/apiserver/plane/app/views/cycle/base.py @@ -187,6 +187,7 @@ def list(self, request, slug, project_id): "completed_issues", "assignee_ids", "status", + "version", "created_by", ) @@ -216,6 +217,7 @@ def list(self, request, slug, project_id): "completed_issues", "assignee_ids", "status", + "version", "created_by", ) return Response(data, status=status.HTTP_200_OK) @@ -255,6 +257,7 @@ def create(self, request, slug, project_id): "external_id", "progress_snapshot", "logo_props", + "version", # meta fields "is_favorite", "total_issues", @@ -306,10 +309,7 @@ def partial_update(self, request, slug, project_id, pk): request_data = request.data - if ( - cycle.end_date is not None - and cycle.end_date < timezone.now() - ): + if cycle.end_date is not None and cycle.end_date < timezone.now(): if "sort_order" in request_data: # Can only change sort order for a completed cycle`` request_data = { @@ -347,6 +347,7 @@ def partial_update(self, request, slug, project_id, pk): "external_id", "progress_snapshot", "logo_props", + "version", # meta fields "is_favorite", "total_issues", @@ -412,6 +413,7 @@ def retrieve(self, request, slug, project_id, pk): "progress_snapshot", "sub_issues", "logo_props", + "version", # meta fields "is_favorite", "total_issues", @@ -1148,6 +1150,7 @@ def get(self, request, slug, project_id, cycle_id): status=status.HTTP_200_OK, ) + class CycleAnalyticsEndpoint(BaseAPIView): @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) diff --git a/apiserver/plane/app/views/workspace/draft.py b/apiserver/plane/app/views/workspace/draft.py index 8ec09d1482a..ad543a75635 100644 --- a/apiserver/plane/app/views/workspace/draft.py +++ b/apiserver/plane/app/views/workspace/draft.py @@ -1,4 +1,10 @@ +# Python imports +import json + # Django imports +from django.utils import timezone +from django.core import serializers +from django.core.serializers.json import DjangoJSONEncoder from django.contrib.postgres.aggregates import ArrayAgg from django.contrib.postgres.fields import ArrayField from django.db.models import ( @@ -26,9 +32,15 @@ from plane.db.models import ( Issue, DraftIssue, + CycleIssue, + ModuleIssue, + DraftIssueModule, + DraftIssueCycle, Workspace, ) from .. import BaseViewSet +from plane.bgtasks.issue_activities_task import issue_activity +from plane.utils.issue_filters import issue_filters class WorkspaceDraftIssueViewSet(BaseViewSet): @@ -40,6 +52,7 @@ class WorkspaceDraftIssueViewSet(BaseViewSet): allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE" ) def list(self, request, slug): + filters = issue_filters(request.query_params, "GET") issues = ( DraftIssue.objects.filter(workspace__slug=slug) .filter(created_by=request.user) @@ -81,8 +94,16 @@ def list(self, request, slug): .order_by("-created_at") ) - serializer = DraftIssueSerializer(issues, many=True) - return Response(serializer.data, status=status.HTTP_200_OK) + issues = issues.filter(**filters) + # List Paginate + return self.paginate( + request=request, + queryset=(issues), + on_results=lambda issues: DraftIssueSerializer( + issues, + many=True, + ).data, + ) @allow_permission( allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE" @@ -94,6 +115,7 @@ def create(self, request, slug): data=request.data, context={ "workspace_id": workspace.id, + "project_id": request.data.get("project_id", None), }, ) if serializer.is_valid(): @@ -156,8 +178,14 @@ def partial_update(self, request, slug, pk): status=status.HTTP_404_NOT_FOUND, ) - serializer = IssueCreateSerializer( - issue, data=request.data, partial=True + serializer = DraftIssueCreateSerializer( + issue, + data=request.data, + partial=True, + context={ + "project_id": request.data.get("project_id", None), + "cycle_id": request.data.get("cycle_id", "not_provided"), + }, ) if serializer.is_valid(): @@ -234,3 +262,151 @@ def destroy(self, request, slug, pk=None): draft_issue = DraftIssue.objects.get(workspace__slug=slug, pk=pk) draft_issue.delete() return Response(status=status.HTTP_204_NO_CONTENT) + + @allow_permission( + allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], + level="WORKSPACE", + ) + def create_draft_to_issue(self, request, slug, draft_id): + draft_issue = ( + DraftIssue.objects.filter(workspace__slug=slug, pk=draft_id) + .annotate(cycle_id=F("draft_issue_cycle__cycle_id")) + .annotate( + label_ids=Coalesce( + ArrayAgg( + "labels__id", + distinct=True, + filter=~Q(labels__id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + assignee_ids=Coalesce( + ArrayAgg( + "assignees__id", + distinct=True, + filter=~Q(assignees__id__isnull=True) + & Q(assignees__member_project__is_active=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + module_ids=Coalesce( + ArrayAgg( + "draft_issue_module__module_id", + distinct=True, + filter=~Q(draft_issue_module__module_id__isnull=True) + & Q( + draft_issue_module__module__archived_at__isnull=True + ), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + ) + .select_related("project", "workspace") + .first() + ) + + if not draft_issue.project_id: + return Response( + {"error": "Project is required to create an issue."}, + status=status.HTTP_400_BAD_REQUEST, + ) + + serializer = IssueCreateSerializer( + data=request.data, + context={ + "project_id": draft_issue.project_id, + "workspace_id": draft_issue.project.workspace_id, + "default_assignee_id": draft_issue.project.default_assignee_id, + }, + ) + + if serializer.is_valid(): + serializer.save() + + issue_activity.delay( + type="issue.activity.created", + requested_data=json.dumps( + self.request.data, cls=DjangoJSONEncoder + ), + actor_id=str(request.user.id), + issue_id=str(serializer.data.get("id", None)), + project_id=str(draft_issue.project_id), + current_instance=None, + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + + if draft_issue.cycle_id: + created_records = CycleIssue.objects.create( + cycle_id=draft_issue.cycle_id, + issue_id=serializer.data.get("id", None), + project_id=draft_issue.project_id, + workspace_id=draft_issue.workspace_id, + created_by_id=draft_issue.created_by_id, + updated_by_id=draft_issue.updated_by_id, + ) + # Capture Issue Activity + issue_activity.delay( + type="cycle.activity.created", + requested_data=None, + actor_id=str(self.request.user.id), + issue_id=None, + project_id=str(self.kwargs.get("project_id", None)), + current_instance=json.dumps( + { + "updated_cycle_issues": None, + "created_cycle_issues": serializers.serialize( + "json", created_records + ), + } + ), + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + + if draft_issue.module_ids: + # bulk create the module + ModuleIssue.objects.bulk_create( + [ + ModuleIssue( + module_id=module, + issue_id=serializer.data.get("id", None), + workspace_id=draft_issue.workspace_id, + project_id=draft_issue.project_id, + created_by_id=draft_issue.created_by_id, + updated_by_id=draft_issue.updated_by_id, + ) + for module in draft_issue.module_ids + ], + batch_size=10, + ) + # Bulk Update the activity + _ = [ + issue_activity.delay( + type="module.activity.created", + requested_data=json.dumps({"module_id": str(module)}), + actor_id=str(request.user.id), + issue_id=serializer.data.get("id", None), + project_id=draft_issue.project_id, + current_instance=None, + epoch=int(timezone.now().timestamp()), + notification=True, + origin=request.META.get("HTTP_ORIGIN"), + ) + for module in draft_issue.module_ids + ] + + # delete the draft issue + draft_issue.delete() + + # delete the draft issue module + DraftIssueModule.objects.filter(draft_issue=draft_issue).delete() + + # delete the draft issue cycle + DraftIssueCycle.objects.filter(draft_issue=draft_issue).delete() + + return Response(serializer.data, status=status.HTTP_201_CREATED) + + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) diff --git a/web/core/store/cycle.store.ts b/web/core/store/cycle.store.ts index e25e3b63be6..2f7dd59cfec 100644 --- a/web/core/store/cycle.store.ts +++ b/web/core/store/cycle.store.ts @@ -144,7 +144,6 @@ export class CycleStore implements ICycleStore { fetchActiveCycleProgress: action, fetchActiveCycleAnalytics: action, fetchCycleDetails: action, - createCycle: action, updateCycleDetails: action, deleteCycle: action, addCycleToFavorites: action, @@ -617,13 +616,15 @@ export class CycleStore implements ICycleStore { * @param data * @returns */ - createCycle = async (workspaceSlug: string, projectId: string, data: Partial) => - await this.cycleService.createCycle(workspaceSlug, projectId, data).then((response) => { - runInAction(() => { - set(this.cycleMap, [response.id], response); - }); - return response; - }); + createCycle = action( + async (workspaceSlug: string, projectId: string, data: Partial) => + await this.cycleService.createCycle(workspaceSlug, projectId, data).then((response) => { + runInAction(() => { + set(this.cycleMap, [response.id], response); + }); + return response; + }) + ); /** * @description updates cycle details