diff --git a/apiserver/plane/app/urls/issue.py b/apiserver/plane/app/urls/issue.py index 234c2824d..63b419c45 100644 --- a/apiserver/plane/app/urls/issue.py +++ b/apiserver/plane/app/urls/issue.py @@ -21,10 +21,16 @@ from plane.app.views import ( IssueArchiveViewSet, IssueRelationViewSet, IssueDraftViewSet, + IssueListEndpoint, ) urlpatterns = [ + path( + "workspaces//projects//issues/list/", + IssueListEndpoint.as_view(), + name="project-issue", + ), path( "workspaces//projects//issues/", IssueViewSet.as_view( diff --git a/apiserver/plane/app/views/__init__.py b/apiserver/plane/app/views/__init__.py index 0a959a667..0340dfa3b 100644 --- a/apiserver/plane/app/views/__init__.py +++ b/apiserver/plane/app/views/__init__.py @@ -85,6 +85,7 @@ from .issue import ( IssueReactionViewSet, IssueRelationViewSet, IssueDraftViewSet, + IssueListEndpoint, ) from .auth_extended import ( diff --git a/apiserver/plane/app/views/issue.py b/apiserver/plane/app/views/issue.py index 26b172e15..ee1ca8456 100644 --- a/apiserver/plane/app/views/issue.py +++ b/apiserver/plane/app/views/issue.py @@ -83,6 +83,7 @@ from plane.utils.issue_filters import issue_filters from collections import defaultdict from plane.utils.cache import cache_path_response, invalidate_path_cache + class IssueViewSet(WebhookMixin, BaseViewSet): def get_serializer_class(self): return ( @@ -1085,7 +1086,7 @@ class IssueArchiveViewSet(BaseViewSet): .filter(workspace__slug=self.kwargs.get("slug")) .select_related("workspace", "project", "state", "parent") .prefetch_related("assignees", "labels", "issue_module__module") - .annotate(cycle_id=F("issue_cycle__cycle_id")) + .annotate(cycle_id=F("issue_cycle__cycle_id")) .annotate( link_count=IssueLink.objects.filter(issue=OuterRef("id")) .order_by() @@ -1133,10 +1134,7 @@ class IssueArchiveViewSet(BaseViewSet): order_by_param = request.GET.get("order_by", "-created_at") - issue_queryset = ( - self.get_queryset() - .filter(**filters) - ) + issue_queryset = self.get_queryset().filter(**filters) # Priority Ordering if order_by_param == "priority" or order_by_param == "-priority": @@ -1218,7 +1216,9 @@ class IssueArchiveViewSet(BaseViewSet): ) return Response(IssueSerializer(issue).data, status=status.HTTP_200_OK) - @invalidate_path_cache("/api/workspaces/:slug/projects/:project_id/archived-issues/", True) + @invalidate_path_cache( + "/api/workspaces/:slug/projects/:project_id/archived-issues/", True + ) def unarchive(self, request, slug, project_id, pk=None): issue = Issue.objects.get( workspace__slug=slug, @@ -1582,15 +1582,17 @@ class IssueRelationViewSet(BaseViewSet): issue_relation = IssueRelation.objects.bulk_create( [ IssueRelation( - issue_id=issue - if relation_type == "blocking" - else issue_id, - related_issue_id=issue_id - if relation_type == "blocking" - else issue, - relation_type="blocked_by" - if relation_type == "blocking" - else relation_type, + issue_id=( + issue if relation_type == "blocking" else issue_id + ), + related_issue_id=( + issue_id if relation_type == "blocking" else issue + ), + relation_type=( + "blocked_by" + if relation_type == "blocking" + else relation_type + ), project_id=project_id, workspace_id=project.workspace_id, created_by=request.user, @@ -1671,9 +1673,7 @@ class IssueDraftViewSet(BaseViewSet): def get_queryset(self): return ( - Issue.objects.filter( - project_id=self.kwargs.get("project_id") - ) + Issue.objects.filter(project_id=self.kwargs.get("project_id")) .filter(workspace__slug=self.kwargs.get("slug")) .filter(is_draft=True) .select_related("workspace", "project", "state", "parent") @@ -1730,10 +1730,7 @@ class IssueDraftViewSet(BaseViewSet): order_by_param = request.GET.get("order_by", "-created_at") - issue_queryset = ( - self.get_queryset() - .filter(**filters) - ) + issue_queryset = self.get_queryset().filter(**filters) # Priority Ordering if order_by_param == "priority" or order_by_param == "-priority": @@ -1832,7 +1829,9 @@ class IssueDraftViewSet(BaseViewSet): issue = ( self.get_queryset().filter(pk=serializer.data["id"]).first() ) - return Response(IssueSerializer(issue).data, status=status.HTTP_201_CREATED) + return Response( + IssueSerializer(issue).data, status=status.HTTP_201_CREATED + ) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) def partial_update(self, request, slug, project_id, pk): @@ -1896,3 +1895,58 @@ class IssueDraftViewSet(BaseViewSet): origin=request.META.get("HTTP_ORIGIN"), ) return Response(status=status.HTTP_204_NO_CONTENT) + + +class IssueListEndpoint(BaseAPIView): + + permission_classes = [ + ProjectEntityPermission, + ] + + def post(self, request, slug, project_id): + issues = request.data.get("issues", []) + + if issues: + return Response( + {"error": "Issues are required"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + queryset = ( + Issue.issue_objects.filter( + workspace__slug=slug, project_id=project_id, pk__in=issues + ) + .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") + ) + ).distinct() + serializer = IssueSerializer(queryset) + return Response(serializer.data, status=status.HTTP_200_OK) diff --git a/apiserver/plane/utils/cache.py b/apiserver/plane/utils/cache.py index 686a68168..24e9067df 100644 --- a/apiserver/plane/utils/cache.py +++ b/apiserver/plane/utils/cache.py @@ -15,6 +15,7 @@ def generate_cache_key(custom_path, auth_header=None): return hashlib.md5(force_bytes(key_data)).hexdigest() def cache_user_response(timeout, path=None): + """decorator to create cache per user""" def decorator(view_func): @wraps(view_func) def _wrapped_view(instance, request, *args, **kwargs): @@ -36,6 +37,7 @@ def cache_user_response(timeout, path=None): return decorator def invalidate_user_cache(path): + """invalidate cache per user""" def decorator(view_func): @wraps(view_func) def _wrapped_view(instance, request, *args, **kwargs): @@ -52,6 +54,7 @@ def invalidate_user_cache(path): def cache_path_response(timeout, path=None): + """Cache path responses""" def decorator(view_func): @wraps(view_func) def _wrapped_view(instance, request, *args, **kwargs): @@ -72,7 +75,9 @@ def cache_path_response(timeout, path=None): return _wrapped_view return decorator + def invalidate_path_cache(path=None, include_url_params=False): + """invalidate path cache responses""" def decorator(view_func): @wraps(view_func) def _wrapped_view(instance, request, *args, **kwargs):