From 93225bae683b150534484b0b66d3256980662932 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Wed, 8 Nov 2023 13:01:37 +0530 Subject: [PATCH 1/4] feat: issue v2 listing endpoint --- apiserver/plane/api/serializers/issue.py | 4 +- apiserver/plane/api/urls/issue.py | 6 +++ apiserver/plane/api/views/__init__.py | 1 + apiserver/plane/api/views/issue.py | 54 ++++++++++++++++++++++++ 4 files changed, 63 insertions(+), 2 deletions(-) diff --git a/apiserver/plane/api/serializers/issue.py b/apiserver/plane/api/serializers/issue.py index f061a0a1938..ae033969fff 100644 --- a/apiserver/plane/api/serializers/issue.py +++ b/apiserver/plane/api/serializers/issue.py @@ -5,7 +5,7 @@ from rest_framework import serializers # Module imports -from .base import BaseSerializer +from .base import BaseSerializer, DynamicBaseSerializer from .user import UserLiteSerializer from .state import StateSerializer, StateLiteSerializer from .project import ProjectLiteSerializer @@ -548,7 +548,7 @@ class Meta: ] -class IssueLiteSerializer(BaseSerializer): +class IssueLiteSerializer(DynamicBaseSerializer): workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace") project_detail = ProjectLiteSerializer(read_only=True, source="project") state_detail = StateLiteSerializer(read_only=True, source="state") diff --git a/apiserver/plane/api/urls/issue.py b/apiserver/plane/api/urls/issue.py index f1ef7c1767e..1b07c894e71 100644 --- a/apiserver/plane/api/urls/issue.py +++ b/apiserver/plane/api/urls/issue.py @@ -3,6 +3,7 @@ from plane.api.views import ( IssueViewSet, + IssueListEndpoint, LabelViewSet, BulkCreateIssueLabelsEndpoint, BulkDeleteIssuesEndpoint, @@ -35,6 +36,11 @@ ), name="project-issue", ), + path( + "v2/workspaces//projects//issues/", + IssueListEndpoint.as_view(), + name="project-issue", + ), path( "workspaces//projects//issues//", IssueViewSet.as_view( diff --git a/apiserver/plane/api/views/__init__.py b/apiserver/plane/api/views/__init__.py index 8f4b2fb9d98..acf8c8972e7 100644 --- a/apiserver/plane/api/views/__init__.py +++ b/apiserver/plane/api/views/__init__.py @@ -65,6 +65,7 @@ from .asset import FileAssetEndpoint, UserAssetsEndpoint from .issue import ( IssueViewSet, + IssueListEndpoint, WorkSpaceIssuesEndpoint, IssueActivityEndpoint, IssueCommentViewSet, diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index 104bdafe291..0c58ff45d05 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -312,6 +312,60 @@ def destroy(self, request, slug, project_id, pk=None): return Response(status=status.HTTP_204_NO_CONTENT) +class IssueListEndpoint(BaseAPIView): + permission_classes = [ + ProjectEntityPermission, + ] + + def get(self, request, slug, project_id): + fields = [field for field in request.GET.get("fields", "").split(",") if field] + filters = issue_filters(request.query_params, "GET") + + # Custom ordering for priority and state + priority_order = ["urgent", "high", "medium", "low", "none"] + state_order = ["backlog", "unstarted", "started", "completed", "cancelled"] + + order_by_param = request.GET.get("order_by", "-created_at") + + issue_queryset = ( + Issue.objects.filter(workspace__slug=slug, project_id=project_id) + .select_related("project") + .select_related("workspace") + .select_related("state") + .select_related("parent") + .prefetch_related("assignees") + .prefetch_related("labels") + .prefetch_related( + Prefetch( + "issue_reactions", + queryset=IssueReaction.objects.select_related("actor"), + ) + ) + .filter(**filters) + .annotate(cycle_id=F("issue_cycle__cycle_id")) + .annotate(module_id=F("issue_module__module_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") + ) + .distinct() + ) + + serializer = IssueLiteSerializer( + issue_queryset, many=True, fields=fields if fields else None + ) + + return Response(serializer.data, status=status.HTTP_200_OK) + + class UserWorkSpaceIssues(BaseAPIView): @method_decorator(gzip_page) def get(self, request, slug): From 871c84a269ed5d5ac4b035ce6b51900aa64150a7 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Wed, 8 Nov 2023 16:18:12 +0530 Subject: [PATCH 2/4] dev: issues v3 endpoint --- apiserver/plane/api/urls/issue.py | 6 ++++ apiserver/plane/api/views/__init__.py | 14 ++++++-- apiserver/plane/api/views/issue.py | 52 +++++++++++++++++++++++---- 3 files changed, 64 insertions(+), 8 deletions(-) diff --git a/apiserver/plane/api/urls/issue.py b/apiserver/plane/api/urls/issue.py index 1b07c894e71..23a8e4fa665 100644 --- a/apiserver/plane/api/urls/issue.py +++ b/apiserver/plane/api/urls/issue.py @@ -4,6 +4,7 @@ from plane.api.views import ( IssueViewSet, IssueListEndpoint, + IssueListGroupedEndpoint, LabelViewSet, BulkCreateIssueLabelsEndpoint, BulkDeleteIssuesEndpoint, @@ -41,6 +42,11 @@ IssueListEndpoint.as_view(), name="project-issue", ), + path( + "v3/workspaces//projects//issues/", + IssueListGroupedEndpoint.as_view(), + name="project-issue", + ), path( "workspaces//projects//issues//", IssueViewSet.as_view( diff --git a/apiserver/plane/api/views/__init__.py b/apiserver/plane/api/views/__init__.py index acf8c8972e7..ca66ce48e8b 100644 --- a/apiserver/plane/api/views/__init__.py +++ b/apiserver/plane/api/views/__init__.py @@ -54,7 +54,12 @@ LeaveWorkspaceEndpoint, ) from .state import StateViewSet -from .view import GlobalViewViewSet, GlobalViewIssuesViewSet, IssueViewViewSet, IssueViewFavoriteViewSet +from .view import ( + GlobalViewViewSet, + GlobalViewIssuesViewSet, + IssueViewViewSet, + IssueViewFavoriteViewSet, +) from .cycle import ( CycleViewSet, CycleIssueViewSet, @@ -66,6 +71,7 @@ from .issue import ( IssueViewSet, IssueListEndpoint, + IssueListGroupedEndpoint, WorkSpaceIssuesEndpoint, IssueActivityEndpoint, IssueCommentViewSet, @@ -163,7 +169,11 @@ DefaultAnalyticsEndpoint, ) -from .notification import NotificationViewSet, UnreadNotificationEndpoint, MarkAllReadNotificationViewSet +from .notification import ( + NotificationViewSet, + UnreadNotificationEndpoint, + MarkAllReadNotificationViewSet, +) from .exporter import ExportIssuesEndpoint diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index 0c58ff45d05..580017ff603 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -321,12 +321,6 @@ def get(self, request, slug, project_id): fields = [field for field in request.GET.get("fields", "").split(",") if field] filters = issue_filters(request.query_params, "GET") - # Custom ordering for priority and state - priority_order = ["urgent", "high", "medium", "low", "none"] - state_order = ["backlog", "unstarted", "started", "completed", "cancelled"] - - order_by_param = request.GET.get("order_by", "-created_at") - issue_queryset = ( Issue.objects.filter(workspace__slug=slug, project_id=project_id) .select_related("project") @@ -366,6 +360,52 @@ def get(self, request, slug, project_id): return Response(serializer.data, status=status.HTTP_200_OK) +class IssueListGroupedEndpoint(BaseAPIView): + + def get(self, request, slug, project_id): + filters = issue_filters(request.query_params, "GET") + fields = [field for field in request.GET.get("fields", "").split(",") if field] + + issue_queryset = ( + Issue.objects.filter(workspace__slug=slug, project_id=project_id) + .select_related("project") + .select_related("workspace") + .select_related("state") + .select_related("parent") + .prefetch_related("assignees") + .prefetch_related("labels") + .prefetch_related( + Prefetch( + "issue_reactions", + queryset=IssueReaction.objects.select_related("actor"), + ) + ) + .filter(**filters) + .annotate(cycle_id=F("issue_cycle__cycle_id")) + .annotate(module_id=F("issue_module__module_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") + ) + .distinct() + ) + + issues = IssueLiteSerializer(issue_queryset, many=True, fields=fields if fields else None).data + grouped_results = group_results(issues, "id", False) + return Response( + grouped_results, + status=status.HTTP_200_OK, + ) + + class UserWorkSpaceIssues(BaseAPIView): @method_decorator(gzip_page) def get(self, request, slug): From 377557b1600e34ea2cca09016e70d23de9aa937c Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Wed, 8 Nov 2023 16:23:24 +0530 Subject: [PATCH 3/4] dev: add permission in the grouped endpoint --- apiserver/plane/api/views/issue.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index 580017ff603..ebfc354934c 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -362,6 +362,10 @@ def get(self, request, slug, project_id): class IssueListGroupedEndpoint(BaseAPIView): + permission_classes = [ + ProjectEntityPermission, + ] + def get(self, request, slug, project_id): filters = issue_filters(request.query_params, "GET") fields = [field for field in request.GET.get("fields", "").split(",") if field] From 5d9769c3baba0b50d1b9172d940b19254a623854 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Wed, 8 Nov 2023 18:05:35 +0530 Subject: [PATCH 4/4] dev: update grouped endpoint --- apiserver/plane/api/views/issue.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index ebfc354934c..d1cd93e73ab 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -403,9 +403,9 @@ def get(self, request, slug, project_id): ) issues = IssueLiteSerializer(issue_queryset, many=True, fields=fields if fields else None).data - grouped_results = group_results(issues, "id", False) + issue_dict = {str(issue["id"]): issue for issue in issues} return Response( - grouped_results, + issue_dict, status=status.HTTP_200_OK, )