From 042cc3160c176457a42a9276af90719b36d8f5fc Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Mon, 19 Feb 2024 11:54:04 +0530 Subject: [PATCH] dev: reduce module endpoints and create a new endpoint for getting issues by list --- apiserver/plane/app/urls/issue.py | 6 ++ apiserver/plane/app/views/__init__.py | 1 + apiserver/plane/app/views/issue.py | 132 +++++++++++++++++++++++++- apiserver/plane/app/views/module.py | 31 +++--- 4 files changed, 149 insertions(+), 21 deletions(-) diff --git a/apiserver/plane/app/urls/issue.py b/apiserver/plane/app/urls/issue.py index 234c2824d..e93f58db3 100644 --- a/apiserver/plane/app/urls/issue.py +++ b/apiserver/plane/app/urls/issue.py @@ -2,6 +2,7 @@ from django.urls import path from plane.app.views import ( + IssueListEndpoint, IssueViewSet, LabelViewSet, BulkCreateIssueLabelsEndpoint, @@ -25,6 +26,11 @@ from plane.app.views import ( 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..79c3f9595 100644 --- a/apiserver/plane/app/views/__init__.py +++ b/apiserver/plane/app/views/__init__.py @@ -67,6 +67,7 @@ from .cycle import ( ) from .asset import FileAssetEndpoint, UserAssetsEndpoint, FileAssetViewSet from .issue import ( + IssueListEndpoint, IssueViewSet, WorkSpaceIssuesEndpoint, IssueActivityEndpoint, diff --git a/apiserver/plane/app/views/issue.py b/apiserver/plane/app/views/issue.py index edefade16..f9cc81150 100644 --- a/apiserver/plane/app/views/issue.py +++ b/apiserver/plane/app/views/issue.py @@ -4,7 +4,6 @@ import random from itertools import chain # Django imports -from django.db import models from django.utils import timezone from django.db.models import ( Prefetch, @@ -82,6 +81,137 @@ from plane.utils.grouper import group_results from plane.utils.issue_filters import issue_filters from collections import defaultdict +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() + + 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 = self.get_queryset().filter(**filters) + + # Priority Ordering + if order_by_param == "priority" or order_by_param == "-priority": + priority_order = ( + priority_order + if order_by_param == "priority" + else priority_order[::-1] + ) + issue_queryset = issue_queryset.annotate( + priority_order=Case( + *[ + When(priority=p, then=Value(i)) + for i, p in enumerate(priority_order) + ], + output_field=CharField(), + ) + ).order_by("priority_order") + + # State Ordering + elif order_by_param in [ + "state__name", + "state__group", + "-state__name", + "-state__group", + ]: + state_order = ( + state_order + if order_by_param in ["state__name", "state__group"] + else state_order[::-1] + ) + issue_queryset = issue_queryset.annotate( + state_order=Case( + *[ + When(state__group=state_group, then=Value(i)) + for i, state_group in enumerate(state_order) + ], + default=Value(len(state_order)), + output_field=CharField(), + ) + ).order_by("state_order") + # assignee and label ordering + elif order_by_param in [ + "labels__name", + "-labels__name", + "assignees__first_name", + "-assignees__first_name", + ]: + issue_queryset = issue_queryset.annotate( + max_values=Max( + order_by_param[1::] + if order_by_param.startswith("-") + else order_by_param + ) + ).order_by( + "-max_values" + if order_by_param.startswith("-") + else "max_values" + ) + else: + issue_queryset = issue_queryset.order_by(order_by_param) + + serializer = IssueSerializer(queryset, many=True, fields=self.fields, expand=self.expand) + return Response(serializer.data, status=status.HTTP_200_OK) + class IssueViewSet(WebhookMixin, BaseViewSet): def get_serializer_class(self): diff --git a/apiserver/plane/app/views/module.py b/apiserver/plane/app/views/module.py index 4792a1f79..95412d26c 100644 --- a/apiserver/plane/app/views/module.py +++ b/apiserver/plane/app/views/module.py @@ -4,10 +4,8 @@ import json # Django Imports from django.utils import timezone from django.db.models import Prefetch, F, OuterRef, Func, Exists, Count, Q -from django.core import serializers from django.utils.decorators import method_decorator from django.views.decorators.gzip import gzip_page -from django.core.serializers.json import DjangoJSONEncoder # Third party imports @@ -38,11 +36,9 @@ from plane.db.models import ( ModuleFavorite, IssueLink, IssueAttachment, - IssueSubscriber, ModuleUserProperties, ) from plane.bgtasks.issue_activites_task import issue_activity -from plane.utils.grouper import group_results from plane.utils.issue_filters import issue_filters from plane.utils.analytics_plot import burndown_plot @@ -62,7 +58,7 @@ class ModuleViewSet(WebhookMixin, BaseViewSet): ) def get_queryset(self): - subquery = ModuleFavorite.objects.filter( + favorite_subquery = ModuleFavorite.objects.filter( user=self.request.user, module_id=OuterRef("pk"), project_id=self.kwargs.get("project_id"), @@ -73,7 +69,7 @@ class ModuleViewSet(WebhookMixin, BaseViewSet): .get_queryset() .filter(project_id=self.kwargs.get("project_id")) .filter(workspace__slug=self.kwargs.get("slug")) - .annotate(is_favorite=Exists(subquery)) + .annotate(is_favorite=Exists(favorite_subquery)) .select_related("project") .select_related("workspace") .select_related("lead") @@ -331,17 +327,16 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet): ProjectEntityPermission, ] - def get_queryset(self): return ( Issue.issue_objects.filter( project_id=self.kwargs.get("project_id"), workspace__slug=self.kwargs.get("slug"), - issue_module__module_id=self.kwargs.get("module_id") + 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("issue_module__module") .annotate(cycle_id=F("issue_cycle__cycle_id")) .annotate( link_count=IssueLink.objects.filter(issue=OuterRef("id")) @@ -384,7 +379,7 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet): # create multiple issues inside a module def create_module_issues(self, request, slug, project_id, module_id): issues = request.data.get("issues", []) - if not len(issues): + if not issues: return Response( {"error": "Issues are required"}, status=status.HTTP_400_BAD_REQUEST, @@ -420,15 +415,12 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet): ) for issue in issues ] - issues = (self.get_queryset().filter(pk__in=issues)) - serializer = IssueSerializer(issues , many=True) - return Response(serializer.data, status=status.HTTP_201_CREATED) - + return Response({"message": "success"}, status=status.HTTP_201_CREATED) # create multiple module inside an issue def create_issue_modules(self, request, slug, project_id, issue_id): modules = request.data.get("modules", []) - if not len(modules): + if not modules: return Response( {"error": "Modules are required"}, status=status.HTTP_400_BAD_REQUEST, @@ -466,10 +458,7 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet): for module in modules ] - issue = (self.get_queryset().filter(pk=issue_id).first()) - serializer = IssueSerializer(issue) - return Response(serializer.data, status=status.HTTP_201_CREATED) - + return Response({"message": "success"}, status=status.HTTP_201_CREATED) def destroy(self, request, slug, project_id, module_id, issue_id): module_issue = ModuleIssue.objects.get( @@ -484,7 +473,9 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet): actor_id=str(request.user.id), issue_id=str(issue_id), project_id=str(project_id), - current_instance=json.dumps({"module_name": module_issue.module.name}), + current_instance=json.dumps( + {"module_name": module_issue.module.name} + ), epoch=int(timezone.now().timestamp()), notification=True, origin=request.META.get("HTTP_ORIGIN"),