diff --git a/apiserver/plane/app/serializers/issue.py b/apiserver/plane/app/serializers/issue.py index 90069bd41..a2cd47e2c 100644 --- a/apiserver/plane/app/serializers/issue.py +++ b/apiserver/plane/app/serializers/issue.py @@ -503,9 +503,7 @@ class IssueCommentSerializer(BaseSerializer): workspace_detail = WorkspaceLiteSerializer( read_only=True, source="workspace" ) - comment_reactions = CommentReactionSerializer( - read_only=True, many=True - ) + comment_reactions = CommentReactionSerializer(read_only=True, many=True) is_member = serializers.BooleanField(read_only=True) class Meta: @@ -558,18 +556,17 @@ class IssueStateSerializer(DynamicBaseSerializer): class IssueSerializer(DynamicBaseSerializer): # ids - project_id = serializers.PrimaryKeyRelatedField(read_only=True) - state_id = serializers.PrimaryKeyRelatedField(read_only=True) - parent_id = serializers.PrimaryKeyRelatedField(read_only=True) cycle_id = serializers.PrimaryKeyRelatedField(read_only=True) - module_ids = serializers.SerializerMethodField() + module_ids = serializers.ListField( + child=serializers.UUIDField(), required=False, allow_null=True + ) # Many to many - label_ids = serializers.PrimaryKeyRelatedField( - read_only=True, many=True, source="labels" + label_ids = serializers.ListField( + child=serializers.UUIDField(), required=False, allow_null=True ) - assignee_ids = serializers.PrimaryKeyRelatedField( - read_only=True, many=True, source="assignees" + assignee_ids = serializers.ListField( + child=serializers.UUIDField(), required=False, allow_null=True ) # Count items @@ -577,9 +574,6 @@ class IssueSerializer(DynamicBaseSerializer): attachment_count = serializers.IntegerField(read_only=True) link_count = serializers.IntegerField(read_only=True) - # is_subscribed - is_subscribed = serializers.BooleanField(read_only=True) - class Meta: model = Issue fields = [ @@ -606,22 +600,19 @@ class IssueSerializer(DynamicBaseSerializer): "updated_by", "attachment_count", "link_count", - "is_subscribed", "is_draft", "archived_at", ] read_only_fields = fields - def get_module_ids(self, obj): - # Access the prefetched modules and extract module IDs - return [module for module in obj.issue_module.values_list("module_id", flat=True)] class IssueDetailSerializer(IssueSerializer): - description_html = serializers.CharField() + description_html = serializers.CharField() + is_subscribed = serializers.BooleanField(read_only=True) class Meta(IssueSerializer.Meta): - fields = IssueSerializer.Meta.fields + ['description_html'] + fields = IssueSerializer.Meta.fields + ["description_html", "is_subscribed"] class IssueLiteSerializer(DynamicBaseSerializer): diff --git a/apiserver/plane/app/urls/issue.py b/apiserver/plane/app/urls/issue.py index e93f58db3..7d661b49e 100644 --- a/apiserver/plane/app/urls/issue.py +++ b/apiserver/plane/app/urls/issue.py @@ -90,11 +90,13 @@ urlpatterns = [ BulkImportIssuesEndpoint.as_view(), name="project-issues-bulk", ), + # deprecated endpoint TODO: remove once confirmed path( "workspaces//my-issues/", UserWorkSpaceIssues.as_view(), name="workspace-issues", ), + ## path( "workspaces//projects//issues//sub-issues/", SubIssuesEndpoint.as_view(), diff --git a/apiserver/plane/app/views/cycle.py b/apiserver/plane/app/views/cycle.py index eaa290662..ad5095f9d 100644 --- a/apiserver/plane/app/views/cycle.py +++ b/apiserver/plane/app/views/cycle.py @@ -708,10 +708,11 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet): ] order_by = request.GET.get("order_by", "created_at") filters = issue_filters(request.query_params, "GET") - issues = ( + queryset = ( Issue.issue_objects.filter(issue_cycle__cycle_id=cycle_id) .filter(project_id=project_id) .filter(workspace__slug=slug) + .filter(**filters) .select_related("workspace", "project", "state", "parent") .prefetch_related( "assignees", @@ -721,7 +722,6 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet): ) .order_by(order_by) .filter(**filters) - .annotate(module_ids=F("issue_module__module_id")) .annotate(cycle_id=F("issue_cycle__cycle_id")) .annotate( link_count=IssueLink.objects.filter(issue=OuterRef("id")) @@ -745,11 +745,67 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet): .annotate(count=Func(F("id"), function="Count")) .values("count") ) + .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), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + module_ids=Coalesce( + ArrayAgg( + "issue_module__module_id", + distinct=True, + filter=~Q(issue_module__module_id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + ) + .order_by(order_by) ) - serializer = IssueSerializer( - issues, many=True, fields=fields if fields else None - ) - return Response(serializer.data, status=status.HTTP_200_OK) + if self.fields: + issues = IssueSerializer( + queryset, many=True, fields=fields if fields else None + ).data + else: + issues = queryset.values( + "id", + "name", + "state_id", + "sort_order", + "completed_at", + "estimate_point", + "priority", + "start_date", + "target_date", + "sequence_id", + "project_id", + "parent_id", + "cycle_id", + "module_ids", + "label_ids", + "assignee_ids", + "sub_issues_count", + "created_at", + "updated_at", + "created_by", + "updated_by", + "attachment_count", + "link_count", + "is_draft", + "archived_at", + ) + return Response(issues, status=status.HTTP_200_OK) def create(self, request, slug, project_id, cycle_id): issues = request.data.get("issues", []) @@ -1121,6 +1177,21 @@ class TransferCycleIssueEndpoint(BaseAPIView): ) .order_by("label_name") ) + + assignee_distribution_data = [ + { + "display_name": item["display_name"], + "assignee_id": ( + str(item["assignee_id"]) if item["assignee_id"] else None + ), + "avatar": item["avatar"], + "total_issues": item["total_issues"], + "completed_issues": item["completed_issues"], + "pending_issues": item["pending_issues"], + } + for item in assignee_distribution + ] + # Label distribution serilization label_distribution_data = [ { diff --git a/apiserver/plane/app/views/inbox.py b/apiserver/plane/app/views/inbox.py index f76c74d9c..5707abea0 100644 --- a/apiserver/plane/app/views/inbox.py +++ b/apiserver/plane/app/views/inbox.py @@ -5,6 +5,10 @@ import json from django.utils import timezone from django.db.models import Q, Count, OuterRef, Func, F, Prefetch 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 Value, UUIDField +from django.db.models.functions import Coalesce # Third party imports from rest_framework import status @@ -92,7 +96,7 @@ class InboxIssueViewSet(BaseViewSet): Issue.objects.filter( project_id=self.kwargs.get("project_id"), workspace__slug=self.kwargs.get("slug"), - issue_inbox__inbox_id=self.kwargs.get("inbox_id") + issue_inbox__inbox_id=self.kwargs.get("inbox_id"), ) .select_related("workspace", "project", "state", "parent") .prefetch_related("assignees", "labels", "issue_module__module") @@ -127,14 +131,75 @@ class InboxIssueViewSet(BaseViewSet): .annotate(count=Func(F("id"), function="Count")) .values("count") ) + .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), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + module_ids=Coalesce( + ArrayAgg( + "issue_module__module_id", + distinct=True, + filter=~Q(issue_module__module_id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + ) ).distinct() def list(self, request, slug, project_id, inbox_id): filters = issue_filters(request.query_params, "GET") - issue_queryset = self.get_queryset().filter(**filters).order_by("issue_inbox__snoozed_till", "issue_inbox__status") - issues_data = IssueSerializer(issue_queryset, expand=self.expand, many=True).data + issue_queryset = ( + self.get_queryset() + .filter(**filters) + .order_by("issue_inbox__snoozed_till", "issue_inbox__status") + ) + if self.expand: + issues = IssueSerializer( + issue_queryset, expand=self.expand, many=True + ).data + else: + issues = issue_queryset.values( + "id", + "name", + "state_id", + "sort_order", + "completed_at", + "estimate_point", + "priority", + "start_date", + "target_date", + "sequence_id", + "project_id", + "parent_id", + "cycle_id", + "module_ids", + "label_ids", + "assignee_ids", + "sub_issues_count", + "created_at", + "updated_at", + "created_by", + "updated_by", + "attachment_count", + "link_count", + "is_draft", + "archived_at", + ) return Response( - issues_data, + issues, status=status.HTTP_200_OK, ) @@ -199,8 +264,8 @@ class InboxIssueViewSet(BaseViewSet): source=request.data.get("source", "in-app"), ) - issue = (self.get_queryset().filter(pk=issue.id).first()) - serializer = IssueSerializer(issue ,expand=self.expand) + issue = self.get_queryset().filter(pk=issue.id).first() + serializer = IssueSerializer(issue, expand=self.expand) return Response(serializer.data, status=status.HTTP_200_OK) def partial_update(self, request, slug, project_id, inbox_id, issue_id): @@ -320,20 +385,23 @@ class InboxIssueViewSet(BaseViewSet): if state is not None: issue.state = state issue.save() - issue = (self.get_queryset().filter(pk=issue_id).first()) + issue = self.get_queryset().filter(pk=issue_id).first() serializer = IssueSerializer(issue, expand=self.expand) return Response(serializer.data, status=status.HTTP_200_OK) return Response( serializer.errors, status=status.HTTP_400_BAD_REQUEST ) else: - issue = (self.get_queryset().filter(pk=issue_id).first()) - serializer = IssueSerializer(issue ,expand=self.expand) + issue = self.get_queryset().filter(pk=issue_id).first() + serializer = IssueSerializer(issue, expand=self.expand) return Response(serializer.data, status=status.HTTP_200_OK) def retrieve(self, request, slug, project_id, inbox_id, issue_id): issue = self.get_queryset().filter(pk=issue_id).first() - serializer = IssueDetailSerializer(issue, expand=self.expand,) + serializer = IssueDetailSerializer( + issue, + expand=self.expand, + ) return Response(serializer.data, status=status.HTTP_200_OK) def destroy(self, request, slug, project_id, inbox_id, issue_id): diff --git a/apiserver/plane/app/views/issue.py b/apiserver/plane/app/views/issue.py index ec87c5b31..fa6fc425e 100644 --- a/apiserver/plane/app/views/issue.py +++ b/apiserver/plane/app/views/issue.py @@ -298,12 +298,6 @@ class IssueViewSet(WebhookMixin, BaseViewSet): .filter(workspace__slug=self.kwargs.get("slug")) .select_related("workspace", "project", "state", "parent") .prefetch_related("assignees", "labels", "issue_module__module") - .prefetch_related( - Prefetch( - "issue_reactions", - queryset=IssueReaction.objects.select_related("actor"), - ) - ) .annotate(cycle_id=F("issue_cycle__cycle_id")) .annotate( link_count=IssueLink.objects.filter(issue=OuterRef("id")) @@ -327,12 +321,40 @@ class IssueViewSet(WebhookMixin, BaseViewSet): .annotate(count=Func(F("id"), function="Count")) .values("count") ) + .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), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + module_ids=Coalesce( + ArrayAgg( + "issue_module__module_id", + distinct=True, + filter=~Q(issue_module__module_id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + ) ).distinct() @method_decorator(gzip_page) def list(self, request, slug, project_id): filters = issue_filters(request.query_params, "GET") + order_by_param = request.GET.get("order_by", "-created_at") + issue_queryset = self.get_queryset().filter(**filters) # Custom ordering for priority and state priority_order = ["urgent", "high", "medium", "low", "none"] state_order = [ @@ -343,10 +365,6 @@ class IssueViewSet(WebhookMixin, BaseViewSet): "cancelled", ] - order_by_param = request.GET.get("order_by", "-created_at") - - issue_queryset = self.get_queryset().filter(**filters) - # Priority Ordering if order_by_param == "priority" or order_by_param == "-priority": priority_order = ( @@ -407,9 +425,42 @@ class IssueViewSet(WebhookMixin, BaseViewSet): else: issue_queryset = issue_queryset.order_by(order_by_param) - issues = IssueSerializer( - issue_queryset, many=True, fields=self.fields, expand=self.expand - ).data + # Only use serializer when expand or fields else return by values + if self.expand or self.fields: + issues = IssueSerializer( + issue_queryset, + many=True, + fields=self.fields, + expand=self.expand, + ).data + else: + issues = issue_queryset.values( + "id", + "name", + "state_id", + "sort_order", + "completed_at", + "estimate_point", + "priority", + "start_date", + "target_date", + "sequence_id", + "project_id", + "parent_id", + "cycle_id", + "module_ids", + "label_ids", + "assignee_ids", + "sub_issues_count", + "created_at", + "updated_at", + "created_by", + "updated_by", + "attachment_count", + "link_count", + "is_draft", + "archived_at", + ) return Response(issues, status=status.HTTP_200_OK) def create(self, request, slug, project_id): @@ -442,10 +493,38 @@ class IssueViewSet(WebhookMixin, BaseViewSet): origin=request.META.get("HTTP_ORIGIN"), ) issue = ( - self.get_queryset().filter(pk=serializer.data["id"]).first() + self.get_queryset() + .filter(pk=serializer.data["id"]) + .values( + "id", + "name", + "state_id", + "sort_order", + "completed_at", + "estimate_point", + "priority", + "start_date", + "target_date", + "sequence_id", + "project_id", + "parent_id", + "cycle_id", + "module_ids", + "label_ids", + "assignee_ids", + "sub_issues_count", + "created_at", + "updated_at", + "created_by", + "updated_by", + "attachment_count", + "link_count", + "is_draft", + "archived_at", + ) + .first() ) - serializer = IssueSerializer(issue) - return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(issue, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) def retrieve(self, request, slug, project_id, pk=None): @@ -481,19 +560,45 @@ class IssueViewSet(WebhookMixin, BaseViewSet): notification=True, origin=request.META.get("HTTP_ORIGIN"), ) - issue = self.get_queryset().filter(pk=pk).first() - return Response( - IssueSerializer(issue).data, status=status.HTTP_200_OK + issue = ( + self.get_queryset() + .filter(pk=pk) + .values( + "id", + "name", + "state_id", + "sort_order", + "completed_at", + "estimate_point", + "priority", + "start_date", + "target_date", + "sequence_id", + "project_id", + "parent_id", + "cycle_id", + "module_ids", + "label_ids", + "assignee_ids", + "sub_issues_count", + "created_at", + "updated_at", + "created_by", + "updated_by", + "attachment_count", + "link_count", + "is_draft", + "archived_at", + ) + .first() ) + return Response(issue, status=status.HTTP_200_OK) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) def destroy(self, request, slug, project_id, pk=None): issue = Issue.objects.get( workspace__slug=slug, project_id=project_id, pk=pk ) - current_instance = json.dumps( - IssueSerializer(issue).data, cls=DjangoJSONEncoder - ) issue.delete() issue_activity.delay( type="issue.activity.deleted", @@ -501,7 +606,7 @@ class IssueViewSet(WebhookMixin, BaseViewSet): actor_id=str(request.user.id), issue_id=str(pk), project_id=str(project_id), - current_instance=current_instance, + current_instance={}, epoch=int(timezone.now().timestamp()), notification=True, origin=request.META.get("HTTP_ORIGIN"), @@ -509,6 +614,7 @@ class IssueViewSet(WebhookMixin, BaseViewSet): return Response(status=status.HTTP_204_NO_CONTENT) +# TODO: deprecated remove once confirmed class UserWorkSpaceIssues(BaseAPIView): @method_decorator(gzip_page) def get(self, request, slug): @@ -563,12 +669,6 @@ class UserWorkSpaceIssues(BaseAPIView): .annotate(count=Func(F("id"), function="Count")) .values("count") ) - .prefetch_related( - Prefetch( - "issue_reactions", - queryset=IssueReaction.objects.select_related("actor"), - ) - ) .filter(**filters) ).distinct() @@ -653,6 +753,7 @@ class UserWorkSpaceIssues(BaseAPIView): return Response(issues, status=status.HTTP_200_OK) +# TODO: deprecated remove once confirmed class WorkSpaceIssuesEndpoint(BaseAPIView): permission_classes = [ WorkSpaceAdminPermission, @@ -955,20 +1056,9 @@ class SubIssuesEndpoint(BaseAPIView): Issue.issue_objects.filter( parent_id=issue_id, workspace__slug=slug ) - .select_related("project") - .select_related("workspace") - .select_related("state") - .select_related("parent") - .prefetch_related("assignees") - .prefetch_related("labels") - .annotate( - sub_issues_count=Issue.issue_objects.filter( - parent=OuterRef("id") - ) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) + .select_related("workspace", "project", "state", "parent") + .prefetch_related("assignees", "labels", "issue_module__module") + .annotate(cycle_id=F("issue_cycle__cycle_id")) .annotate( link_count=IssueLink.objects.filter(issue=OuterRef("id")) .order_by() @@ -983,11 +1073,39 @@ class SubIssuesEndpoint(BaseAPIView): .annotate(count=Func(F("id"), function="Count")) .values("count") ) - .prefetch_related( - Prefetch( - "issue_reactions", - queryset=IssueReaction.objects.select_related("actor"), + .annotate( + sub_issues_count=Issue.issue_objects.filter( + parent=OuterRef("id") ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .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), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + module_ids=Coalesce( + ArrayAgg( + "issue_module__module_id", + distinct=True, + filter=~Q(issue_module__module_id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), ) .annotate(state_group=F("state__group")) ) @@ -997,13 +1115,36 @@ class SubIssuesEndpoint(BaseAPIView): for sub_issue in sub_issues: result[sub_issue.state_group].append(str(sub_issue.id)) - serializer = IssueSerializer( - sub_issues, - many=True, + sub_issues = sub_issues.values( + "id", + "name", + "state_id", + "sort_order", + "completed_at", + "estimate_point", + "priority", + "start_date", + "target_date", + "sequence_id", + "project_id", + "parent_id", + "cycle_id", + "module_ids", + "label_ids", + "assignee_ids", + "sub_issues_count", + "created_at", + "updated_at", + "created_by", + "updated_by", + "attachment_count", + "link_count", + "is_draft", + "archived_at", ) return Response( { - "sub_issues": serializer.data, + "sub_issues": sub_issues, "state_distribution": result, }, status=status.HTTP_200_OK, @@ -1291,15 +1432,36 @@ class IssueArchiveViewSet(BaseViewSet): .annotate(count=Func(F("id"), function="Count")) .values("count") ) + .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), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + module_ids=Coalesce( + ArrayAgg( + "issue_module__module_id", + distinct=True, + filter=~Q(issue_module__module_id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + ) ) @method_decorator(gzip_page) def list(self, request, slug, project_id): - fields = [ - field - for field in request.GET.get("fields", "").split(",") - if field - ] filters = issue_filters(request.query_params, "GET") show_sub_issues = request.GET.get("show_sub_issues", "true") @@ -1382,10 +1544,40 @@ class IssueArchiveViewSet(BaseViewSet): if show_sub_issues == "true" else issue_queryset.filter(parent__isnull=True) ) - - issues = IssueSerializer( - issue_queryset, many=True, fields=fields if fields else None - ).data + if self.expand or self.fields: + issues = IssueSerializer( + issue_queryset, + many=True, + fields=self.fields, + ).data + else: + issues = issue_queryset.values( + "id", + "name", + "state_id", + "sort_order", + "completed_at", + "estimate_point", + "priority", + "start_date", + "target_date", + "sequence_id", + "project_id", + "parent_id", + "cycle_id", + "module_ids", + "label_ids", + "assignee_ids", + "sub_issues_count", + "created_at", + "updated_at", + "created_by", + "updated_by", + "attachment_count", + "link_count", + "is_draft", + "archived_at", + ) return Response(issues, status=status.HTTP_200_OK) def retrieve(self, request, slug, project_id, pk=None): @@ -1856,12 +2048,6 @@ class IssueDraftViewSet(BaseViewSet): .filter(is_draft=True) .select_related("workspace", "project", "state", "parent") .prefetch_related("assignees", "labels", "issue_module__module") - .prefetch_related( - Prefetch( - "issue_reactions", - queryset=IssueReaction.objects.select_related("actor"), - ) - ) .annotate(cycle_id=F("issue_cycle__cycle_id")) .annotate( link_count=IssueLink.objects.filter(issue=OuterRef("id")) @@ -1885,6 +2071,32 @@ class IssueDraftViewSet(BaseViewSet): .annotate(count=Func(F("id"), function="Count")) .values("count") ) + .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), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + module_ids=Coalesce( + ArrayAgg( + "issue_module__module_id", + distinct=True, + filter=~Q(issue_module__module_id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + ) ).distinct() @method_decorator(gzip_page) @@ -1970,9 +2182,42 @@ class IssueDraftViewSet(BaseViewSet): else: issue_queryset = issue_queryset.order_by(order_by_param) - issues = IssueSerializer( - issue_queryset, many=True, fields=fields if fields else None - ).data + # Only use serializer when expand else return by values + if self.expand or self.fields: + issues = IssueSerializer( + issue_queryset, + many=True, + fields=self.fields, + expand=self.expand, + ).data + else: + issues = issue_queryset.values( + "id", + "name", + "state_id", + "sort_order", + "completed_at", + "estimate_point", + "priority", + "start_date", + "target_date", + "sequence_id", + "project_id", + "parent_id", + "cycle_id", + "module_ids", + "label_ids", + "assignee_ids", + "sub_issues_count", + "created_at", + "updated_at", + "created_by", + "updated_by", + "attachment_count", + "link_count", + "is_draft", + "archived_at", + ) return Response(issues, status=status.HTTP_200_OK) def create(self, request, slug, project_id): @@ -2057,9 +2302,6 @@ class IssueDraftViewSet(BaseViewSet): issue = Issue.objects.get( workspace__slug=slug, project_id=project_id, pk=pk ) - current_instance = json.dumps( - IssueSerializer(issue).data, cls=DjangoJSONEncoder - ) issue.delete() issue_activity.delay( type="issue_draft.activity.deleted", @@ -2067,7 +2309,7 @@ class IssueDraftViewSet(BaseViewSet): actor_id=str(request.user.id), issue_id=str(pk), project_id=str(project_id), - current_instance=current_instance, + current_instance={}, epoch=int(timezone.now().timestamp()), notification=True, origin=request.META.get("HTTP_ORIGIN"), diff --git a/apiserver/plane/app/views/module.py b/apiserver/plane/app/views/module.py index 41442579f..5ac244dda 100644 --- a/apiserver/plane/app/views/module.py +++ b/apiserver/plane/app/views/module.py @@ -449,8 +449,7 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet): issue_module__module_id=self.kwargs.get("module_id"), ) .select_related("workspace", "project", "state", "parent") - .prefetch_related("labels", "assignees") - .prefetch_related("issue_module__module") + .prefetch_related("assignees", "labels", "issue_module__module") .annotate(cycle_id=F("issue_cycle__cycle_id")) .annotate( link_count=IssueLink.objects.filter(issue=OuterRef("id")) @@ -474,6 +473,32 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet): .annotate(count=Func(F("id"), function="Count")) .values("count") ) + .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), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + module_ids=Coalesce( + ArrayAgg( + "issue_module__module_id", + distinct=True, + filter=~Q(issue_module__module_id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + ) ).distinct() @method_decorator(gzip_page) @@ -485,10 +510,39 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet): ] filters = issue_filters(request.query_params, "GET") issue_queryset = self.get_queryset().filter(**filters) - serializer = IssueSerializer( - issue_queryset, many=True, fields=fields if fields else None - ) - return Response(serializer.data, status=status.HTTP_200_OK) + if self.fields or self.expand: + issues = IssueSerializer( + issue_queryset, many=True, fields=fields if fields else None + ).data + else: + issues = issue_queryset.values( + "id", + "name", + "state_id", + "sort_order", + "completed_at", + "estimate_point", + "priority", + "start_date", + "target_date", + "sequence_id", + "project_id", + "parent_id", + "cycle_id", + "module_ids", + "label_ids", + "assignee_ids", + "sub_issues_count", + "created_at", + "updated_at", + "created_by", + "updated_by", + "attachment_count", + "link_count", + "is_draft", + "archived_at", + ) + return Response(issues, status=status.HTTP_200_OK) # create multiple issues inside a module def create_module_issues(self, request, slug, project_id, module_id): diff --git a/apiserver/plane/app/views/view.py b/apiserver/plane/app/views/view.py index 27f31f7a9..97a0f036f 100644 --- a/apiserver/plane/app/views/view.py +++ b/apiserver/plane/app/views/view.py @@ -1,6 +1,6 @@ # Django imports from django.db.models import ( - Prefetch, + Q, OuterRef, Func, F, @@ -13,16 +13,21 @@ from django.db.models import ( ) from django.utils.decorators import method_decorator from django.views.decorators.gzip import gzip_page -from django.db.models import Prefetch, OuterRef, Exists +from django.contrib.postgres.aggregates import ArrayAgg +from django.contrib.postgres.fields import ArrayField +from django.db.models import Value, UUIDField +from django.db.models.functions import Coalesce +from django.contrib.postgres.aggregates import ArrayAgg +from django.contrib.postgres.fields import ArrayField +from django.db.models import Value, UUIDField # Third party imports from rest_framework.response import Response from rest_framework import status # Module imports -from . import BaseViewSet, BaseAPIView +from . import BaseViewSet from plane.app.serializers import ( - GlobalViewSerializer, IssueViewSerializer, IssueSerializer, IssueViewFavoriteSerializer, @@ -30,22 +35,16 @@ from plane.app.serializers import ( from plane.app.permissions import ( WorkspaceEntityPermission, ProjectEntityPermission, - WorkspaceViewerPermission, - ProjectLitePermission, ) from plane.db.models import ( Workspace, - GlobalView, IssueView, Issue, IssueViewFavorite, - IssueReaction, IssueLink, IssueAttachment, - IssueSubscriber, ) from plane.utils.issue_filters import issue_filters -from plane.utils.grouper import group_results class GlobalViewViewSet(BaseViewSet): @@ -89,11 +88,54 @@ class GlobalViewIssuesViewSet(BaseViewSet): .filter(workspace__slug=self.kwargs.get("slug")) .select_related("workspace", "project", "state", "parent") .prefetch_related("assignees", "labels", "issue_module__module") - .prefetch_related( - Prefetch( - "issue_reactions", - queryset=IssueReaction.objects.select_related("actor"), + .annotate(cycle_id=F("issue_cycle__cycle_id")) + .annotate( + link_count=IssueLink.objects.filter(issue=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + attachment_count=IssueAttachment.objects.filter( + issue=OuterRef("id") ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .annotate( + sub_issues_count=Issue.issue_objects.filter( + parent=OuterRef("id") + ) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .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), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + module_ids=Coalesce( + ArrayAgg( + "issue_module__module_id", + distinct=True, + filter=~Q(issue_module__module_id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), ) ) @@ -123,28 +165,6 @@ class GlobalViewIssuesViewSet(BaseViewSet): .filter(**filters) .filter(project__project_projectmember__member=self.request.user) .annotate(cycle_id=F("issue_cycle__cycle_id")) - .annotate( - link_count=IssueLink.objects.filter(issue=OuterRef("id")) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate( - attachment_count=IssueAttachment.objects.filter( - issue=OuterRef("id") - ) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate( - sub_issues_count=Issue.issue_objects.filter( - parent=OuterRef("id") - ) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) ) # Priority Ordering @@ -207,10 +227,39 @@ class GlobalViewIssuesViewSet(BaseViewSet): else: issue_queryset = issue_queryset.order_by(order_by_param) - serializer = IssueSerializer( - issue_queryset, many=True, fields=fields if fields else None - ) - return Response(serializer.data, status=status.HTTP_200_OK) + if self.fields: + issues = IssueSerializer( + issue_queryset, many=True, fields=self.fields + ).data + else: + issues = issue_queryset.values( + "id", + "name", + "state_id", + "sort_order", + "completed_at", + "estimate_point", + "priority", + "start_date", + "target_date", + "sequence_id", + "project_id", + "parent_id", + "cycle_id", + "module_ids", + "label_ids", + "assignee_ids", + "sub_issues_count", + "created_at", + "updated_at", + "created_by", + "updated_by", + "attachment_count", + "link_count", + "is_draft", + "archived_at", + ) + return Response(issues, status=status.HTTP_200_OK) class IssueViewViewSet(BaseViewSet): diff --git a/apiserver/plane/app/views/workspace.py b/apiserver/plane/app/views/workspace.py index ba9314a75..77f96dd2a 100644 --- a/apiserver/plane/app/views/workspace.py +++ b/apiserver/plane/app/views/workspace.py @@ -26,6 +26,10 @@ from django.db.models import ( ) from django.db.models.functions import ExtractWeek, Cast, ExtractDay from django.db.models.fields import DateField +from django.contrib.postgres.aggregates import ArrayAgg +from django.contrib.postgres.fields import ArrayField +from django.db.models import Value, UUIDField +from django.db.models.functions import Coalesce # Third party modules from rest_framework import status @@ -1380,6 +1384,32 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView): .annotate(count=Func(F("id"), function="Count")) .values("count") ) + .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), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + module_ids=Coalesce( + ArrayAgg( + "issue_module__module_id", + distinct=True, + filter=~Q(issue_module__module_id__isnull=True), + ), + Value([], output_field=ArrayField(UUIDField())), + ), + ) .order_by("created_at") ).distinct() diff --git a/web/components/issues/issue-detail/subscription.tsx b/web/components/issues/issue-detail/subscription.tsx index ebd213dac..603b3ada7 100644 --- a/web/components/issues/issue-detail/subscription.tsx +++ b/web/components/issues/issue-detail/subscription.tsx @@ -48,6 +48,13 @@ export const IssueSubscription: FC = observer((props) => { } }; + if (!subscription) + return ( + + + + ); + return ( <> {subscription ? (