diff --git a/apiserver/plane/api/serializers/issue.py b/apiserver/plane/api/serializers/issue.py index f061a0a19..ae033969f 100644 --- a/apiserver/plane/api/serializers/issue.py +++ b/apiserver/plane/api/serializers/issue.py @@ -5,7 +5,7 @@ from django.utils import timezone 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 IssueSerializer(BaseSerializer): ] -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 f1ef7c176..23a8e4fa6 100644 --- a/apiserver/plane/api/urls/issue.py +++ b/apiserver/plane/api/urls/issue.py @@ -3,6 +3,8 @@ from django.urls import path from plane.api.views import ( IssueViewSet, + IssueListEndpoint, + IssueListGroupedEndpoint, LabelViewSet, BulkCreateIssueLabelsEndpoint, BulkDeleteIssuesEndpoint, @@ -35,6 +37,16 @@ urlpatterns = [ ), name="project-issue", ), + path( + "v2/workspaces//projects//issues/", + 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 8f4b2fb9d..ca66ce48e 100644 --- a/apiserver/plane/api/views/__init__.py +++ b/apiserver/plane/api/views/__init__.py @@ -54,7 +54,12 @@ from .workspace import ( LeaveWorkspaceEndpoint, ) from .state import StateViewSet -from .view import GlobalViewViewSet, GlobalViewIssuesViewSet, IssueViewViewSet, IssueViewFavoriteViewSet +from .view import ( + GlobalViewViewSet, + GlobalViewIssuesViewSet, + IssueViewViewSet, + IssueViewFavoriteViewSet, +) from .cycle import ( CycleViewSet, CycleIssueViewSet, @@ -65,6 +70,8 @@ from .cycle import ( from .asset import FileAssetEndpoint, UserAssetsEndpoint from .issue import ( IssueViewSet, + IssueListEndpoint, + IssueListGroupedEndpoint, WorkSpaceIssuesEndpoint, IssueActivityEndpoint, IssueCommentViewSet, @@ -162,7 +169,11 @@ from .analytic import ( 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 104bdafe2..d1cd93e73 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -312,6 +312,104 @@ class IssueViewSet(BaseViewSet): 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") + + 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 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] + + 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 + issue_dict = {str(issue["id"]): issue for issue in issues} + return Response( + issue_dict, + status=status.HTTP_200_OK, + ) + + class UserWorkSpaceIssues(BaseAPIView): @method_decorator(gzip_page) def get(self, request, slug):