diff --git a/apiserver/plane/app/views/cycle/archive.py b/apiserver/plane/app/views/cycle/archive.py index dc5f6a57308..947f43a5f8d 100644 --- a/apiserver/plane/app/views/cycle/archive.py +++ b/apiserver/plane/app/views/cycle/archive.py @@ -14,21 +14,18 @@ UUIDField, Value, When, + Subquery, + Sum, + FloatField, ) -from django.db.models.functions import Coalesce +from django.db.models.functions import Coalesce, Cast from django.utils import timezone # Third party imports from rest_framework import status from rest_framework.response import Response from plane.app.permissions import ProjectEntityPermission -from plane.db.models import ( - Cycle, - UserFavorite, - Issue, - Label, - User, -) +from plane.db.models import Cycle, UserFavorite, Issue, Label, User, Project from plane.utils.analytics_plot import burndown_plot # Module imports @@ -49,6 +46,89 @@ def get_queryset(self): project_id=self.kwargs.get("project_id"), workspace__slug=self.kwargs.get("slug"), ) + backlog_estimate_point = ( + Issue.issue_objects.filter( + estimate_point__estimate__type="points", + state__group="backlog", + issue_cycle__cycle_id=OuterRef("pk"), + ) + .values("issue_cycle__cycle_id") + .annotate( + backlog_estimate_point=Sum( + Cast("estimate_point__value", FloatField()) + ) + ) + .values("backlog_estimate_point")[:1] + ) + unstarted_estimate_point = ( + Issue.issue_objects.filter( + estimate_point__estimate__type="points", + state__group="unstarted", + issue_cycle__cycle_id=OuterRef("pk"), + ) + .values("issue_cycle__cycle_id") + .annotate( + unstarted_estimate_point=Sum( + Cast("estimate_point__value", FloatField()) + ) + ) + .values("unstarted_estimate_point")[:1] + ) + started_estimate_point = ( + Issue.issue_objects.filter( + estimate_point__estimate__type="points", + state__group="started", + issue_cycle__cycle_id=OuterRef("pk"), + ) + .values("issue_cycle__cycle_id") + .annotate( + started_estimate_point=Sum( + Cast("estimate_point__value", FloatField()) + ) + ) + .values("started_estimate_point")[:1] + ) + cancelled_estimate_point = ( + Issue.issue_objects.filter( + estimate_point__estimate__type="points", + state__group="cancelled", + issue_cycle__cycle_id=OuterRef("pk"), + ) + .values("issue_cycle__cycle_id") + .annotate( + cancelled_estimate_point=Sum( + Cast("estimate_point__value", FloatField()) + ) + ) + .values("cancelled_estimate_point")[:1] + ) + completed_estimate_point = ( + Issue.issue_objects.filter( + estimate_point__estimate__type="points", + state__group="completed", + issue_cycle__cycle_id=OuterRef("pk"), + ) + .values("issue_cycle__cycle_id") + .annotate( + completed_estimate_points=Sum( + Cast("estimate_point__value", FloatField()) + ) + ) + .values("completed_estimate_points")[:1] + ) + total_estimate_point = ( + Issue.issue_objects.filter( + estimate_point__estimate__type="points", + issue_cycle__cycle_id=OuterRef("pk"), + ) + .values("issue_cycle__cycle_id") + .annotate( + total_estimate_points=Sum( + Cast("estimate_point__value", FloatField()) + ) + ) + .values("total_estimate_points")[:1] + ) return ( Cycle.objects.filter(workspace__slug=self.kwargs.get("slug")) .filter(project_id=self.kwargs.get("project_id")) @@ -172,6 +252,42 @@ def get_queryset(self): Value([], output_field=ArrayField(UUIDField())), ) ) + .annotate( + backlog_estimate_points=Coalesce( + Subquery(backlog_estimate_point), + Value(0, output_field=FloatField()), + ), + ) + .annotate( + unstarted_estimate_points=Coalesce( + Subquery(unstarted_estimate_point), + Value(0, output_field=FloatField()), + ), + ) + .annotate( + started_estimate_points=Coalesce( + Subquery(started_estimate_point), + Value(0, output_field=FloatField()), + ), + ) + .annotate( + cancelled_estimate_points=Coalesce( + Subquery(cancelled_estimate_point), + Value(0, output_field=FloatField()), + ), + ) + .annotate( + completed_estimate_points=Coalesce( + Subquery(completed_estimate_point), + Value(0, output_field=FloatField()), + ), + ) + .annotate( + total_estimate_points=Coalesce( + Subquery(total_estimate_point), + Value(0, output_field=FloatField()), + ), + ) .order_by("-is_favorite", "name") .distinct() ) @@ -179,17 +295,7 @@ def get_queryset(self): def get(self, request, slug, project_id, pk=None): if pk is None: queryset = ( - self.get_queryset() - .annotate( - total_issues=Count( - "issue_cycle", - filter=Q( - issue_cycle__issue__archived_at__isnull=True, - issue_cycle__issue__is_draft=False, - ), - ) - ) - .values( + self.get_queryset().values( # necessary fields "id", "workspace_id", @@ -255,7 +361,10 @@ def get(self, request, slug, project_id, pk=None): "external_id", "progress_snapshot", "sub_issues", + "logo_props", # meta fields + "completed_estimate_points", + "total_estimate_points", "is_favorite", "total_issues", "cancelled_issues", @@ -265,17 +374,114 @@ def get(self, request, slug, project_id, pk=None): "backlog_issues", "assignee_ids", "status", + "created_by", + "archived_at", ) .first() ) queryset = queryset.first() - if data is None: - return Response( - {"error": "Cycle does not exist"}, - status=status.HTTP_400_BAD_REQUEST, + estimate_type = Project.objects.filter( + workspace__slug=slug, + pk=project_id, + estimate__isnull=False, + estimate__type="points", + ).exists() + + data["estimate_distribution"] = {} + if estimate_type: + assignee_distribution = ( + Issue.issue_objects.filter( + issue_cycle__cycle_id=pk, + workspace__slug=slug, + project_id=project_id, + ) + .annotate(display_name=F("assignees__display_name")) + .annotate(assignee_id=F("assignees__id")) + .annotate(avatar=F("assignees__avatar")) + .values("display_name", "assignee_id", "avatar") + .annotate( + total_estimates=Sum( + Cast("estimate_point__value", FloatField()) + ) + ) + .annotate( + completed_estimates=Sum( + Cast("estimate_point__value", FloatField()), + filter=Q( + completed_at__isnull=False, + archived_at__isnull=True, + is_draft=False, + ), + ) + ) + .annotate( + pending_estimates=Sum( + Cast("estimate_point__value", FloatField()), + filter=Q( + completed_at__isnull=True, + archived_at__isnull=True, + is_draft=False, + ), + ) + ) + .order_by("display_name") ) + label_distribution = ( + Issue.issue_objects.filter( + issue_cycle__cycle_id=pk, + workspace__slug=slug, + project_id=project_id, + ) + .annotate(label_name=F("labels__name")) + .annotate(color=F("labels__color")) + .annotate(label_id=F("labels__id")) + .values("label_name", "color", "label_id") + .annotate( + total_estimates=Sum( + Cast("estimate_point__value", FloatField()) + ) + ) + .annotate( + completed_estimates=Sum( + Cast("estimate_point__value", FloatField()), + filter=Q( + completed_at__isnull=False, + archived_at__isnull=True, + is_draft=False, + ), + ) + ) + .annotate( + pending_estimates=Sum( + Cast("estimate_point__value", FloatField()), + filter=Q( + completed_at__isnull=True, + archived_at__isnull=True, + is_draft=False, + ), + ) + ) + .order_by("label_name") + ) + data["estimate_distribution"] = { + "assignees": assignee_distribution, + "labels": label_distribution, + "completion_chart": {}, + } + + if data["start_date"] and data["end_date"]: + data["estimate_distribution"]["completion_chart"] = ( + burndown_plot( + queryset=queryset, + slug=slug, + project_id=project_id, + plot_type="points", + cycle_id=pk, + ) + ) + # Assignee Distribution assignee_distribution = ( Issue.issue_objects.filter( @@ -298,7 +504,10 @@ def get(self, request, slug, project_id, pk=None): .annotate( total_issues=Count( "id", - filter=Q(archived_at__isnull=True, is_draft=False), + filter=Q( + archived_at__isnull=True, + is_draft=False, + ), ), ) .annotate( @@ -338,7 +547,10 @@ def get(self, request, slug, project_id, pk=None): .annotate( total_issues=Count( "id", - filter=Q(archived_at__isnull=True, is_draft=False), + filter=Q( + archived_at__isnull=True, + is_draft=False, + ), ), ) .annotate( diff --git a/apiserver/plane/app/views/cycle/base.py b/apiserver/plane/app/views/cycle/base.py index 76d5a75f88e..28ce31a083b 100644 --- a/apiserver/plane/app/views/cycle/base.py +++ b/apiserver/plane/app/views/cycle/base.py @@ -384,7 +384,7 @@ def list(self, request, slug, project_id): data[0]["estimate_distribution"] = {} if estimate_type: assignee_distribution = ( - Issue.objects.filter( + Issue.issue_objects.filter( issue_cycle__cycle_id=data[0]["id"], workspace__slug=slug, project_id=project_id, @@ -422,7 +422,7 @@ def list(self, request, slug, project_id): ) label_distribution = ( - Issue.objects.filter( + Issue.issue_objects.filter( issue_cycle__cycle_id=data[0]["id"], workspace__slug=slug, project_id=project_id, @@ -476,7 +476,7 @@ def list(self, request, slug, project_id): ) assignee_distribution = ( - Issue.objects.filter( + Issue.issue_objects.filter( issue_cycle__cycle_id=data[0]["id"], workspace__slug=slug, project_id=project_id, @@ -518,7 +518,7 @@ def list(self, request, slug, project_id): ) label_distribution = ( - Issue.objects.filter( + Issue.issue_objects.filter( issue_cycle__cycle_id=data[0]["id"], workspace__slug=slug, project_id=project_id, @@ -833,7 +833,7 @@ def retrieve(self, request, slug, project_id, pk): data["estimate_distribution"] = {} if estimate_type: assignee_distribution = ( - Issue.objects.filter( + Issue.issue_objects.filter( issue_cycle__cycle_id=pk, workspace__slug=slug, project_id=project_id, @@ -871,7 +871,7 @@ def retrieve(self, request, slug, project_id, pk): ) label_distribution = ( - Issue.objects.filter( + Issue.issue_objects.filter( issue_cycle__cycle_id=pk, workspace__slug=slug, project_id=project_id, @@ -926,7 +926,7 @@ def retrieve(self, request, slug, project_id, pk): # Assignee Distribution assignee_distribution = ( - Issue.objects.filter( + Issue.issue_objects.filter( issue_cycle__cycle_id=pk, workspace__slug=slug, project_id=project_id, @@ -977,7 +977,7 @@ def retrieve(self, request, slug, project_id, pk): # Label Distribution label_distribution = ( - Issue.objects.filter( + Issue.issue_objects.filter( issue_cycle__cycle_id=pk, workspace__slug=slug, project_id=project_id, diff --git a/apiserver/plane/app/views/module/archive.py b/apiserver/plane/app/views/module/archive.py index 8f6d9c6b0e3..243c680cad1 100644 --- a/apiserver/plane/app/views/module/archive.py +++ b/apiserver/plane/app/views/module/archive.py @@ -12,7 +12,8 @@ Subquery, UUIDField, Value, - Sum + Sum, + FloatField, ) from django.db.models.functions import Coalesce, Cast from django.utils import timezone @@ -44,8 +45,8 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView): def get_queryset(self): favorite_subquery = UserFavorite.objects.filter( user=self.request.user, - entity_identifier=OuterRef("pk"), entity_type="module", + entity_identifier=OuterRef("pk"), project_id=self.kwargs.get("project_id"), workspace__slug=self.kwargs.get("slug"), ) @@ -102,8 +103,93 @@ def get_queryset(self): .annotate(cnt=Count("pk")) .values("cnt") ) + completed_estimate_point = ( + Issue.issue_objects.filter( + estimate_point__estimate__type="points", + state__group="completed", + issue_module__module_id=OuterRef("pk"), + ) + .values("issue_module__module_id") + .annotate( + completed_estimate_points=Sum( + Cast("estimate_point__value", FloatField()) + ) + ) + .values("completed_estimate_points")[:1] + ) + + total_estimate_point = ( + Issue.issue_objects.filter( + estimate_point__estimate__type="points", + issue_module__module_id=OuterRef("pk"), + ) + .values("issue_module__module_id") + .annotate( + total_estimate_points=Sum( + Cast("estimate_point__value", FloatField()) + ) + ) + .values("total_estimate_points")[:1] + ) + backlog_estimate_point = ( + Issue.issue_objects.filter( + estimate_point__estimate__type="points", + state__group="backlog", + issue_module__module_id=OuterRef("pk"), + ) + .values("issue_module__module_id") + .annotate( + backlog_estimate_point=Sum( + Cast("estimate_point__value", FloatField()) + ) + ) + .values("backlog_estimate_point")[:1] + ) + unstarted_estimate_point = ( + Issue.issue_objects.filter( + estimate_point__estimate__type="points", + state__group="unstarted", + issue_module__module_id=OuterRef("pk"), + ) + .values("issue_module__module_id") + .annotate( + unstarted_estimate_point=Sum( + Cast("estimate_point__value", FloatField()) + ) + ) + .values("unstarted_estimate_point")[:1] + ) + started_estimate_point = ( + Issue.issue_objects.filter( + estimate_point__estimate__type="points", + state__group="started", + issue_module__module_id=OuterRef("pk"), + ) + .values("issue_module__module_id") + .annotate( + started_estimate_point=Sum( + Cast("estimate_point__value", FloatField()) + ) + ) + .values("started_estimate_point")[:1] + ) + cancelled_estimate_point = ( + Issue.issue_objects.filter( + estimate_point__estimate__type="points", + state__group="cancelled", + issue_module__module_id=OuterRef("pk"), + ) + .values("issue_module__module_id") + .annotate( + cancelled_estimate_point=Sum( + Cast("estimate_point__value", FloatField()) + ) + ) + .values("cancelled_estimate_point")[:1] + ) return ( Module.objects.filter(workspace__slug=self.kwargs.get("slug")) + .filter(project_id=self.kwargs.get("project_id")) .filter(archived_at__isnull=False) .annotate(is_favorite=Exists(favorite_subquery)) .select_related("workspace", "project", "lead") @@ -152,6 +238,42 @@ def get_queryset(self): Value(0, output_field=IntegerField()), ) ) + .annotate( + backlog_estimate_points=Coalesce( + Subquery(backlog_estimate_point), + Value(0, output_field=FloatField()), + ), + ) + .annotate( + unstarted_estimate_points=Coalesce( + Subquery(unstarted_estimate_point), + Value(0, output_field=FloatField()), + ), + ) + .annotate( + started_estimate_points=Coalesce( + Subquery(started_estimate_point), + Value(0, output_field=FloatField()), + ), + ) + .annotate( + cancelled_estimate_points=Coalesce( + Subquery(cancelled_estimate_point), + Value(0, output_field=FloatField()), + ), + ) + .annotate( + completed_estimate_points=Coalesce( + Subquery(completed_estimate_point), + Value(0, output_field=FloatField()), + ), + ) + .annotate( + total_estimate_points=Coalesce( + Subquery(total_estimate_point), + Value(0, output_field=FloatField()), + ), + ) .annotate( member_ids=Coalesce( ArrayAgg( @@ -232,7 +354,7 @@ def get(self, request, slug, project_id, pk=None): data["estimate_distribution"] = {} if estimate_type: - label_distribution = ( + assignee_distribution = ( Issue.issue_objects.filter( issue_module__module_id=pk, workspace__slug=slug, @@ -252,12 +374,12 @@ def get(self, request, slug, project_id, pk=None): ) .annotate( total_estimates=Sum( - Cast("estimate_point__value", IntegerField()) + Cast("estimate_point__value", FloatField()) ), ) .annotate( completed_estimates=Sum( - Cast("estimate_point__value", IntegerField()), + Cast("estimate_point__value", FloatField()), filter=Q( completed_at__isnull=False, archived_at__isnull=True, @@ -267,7 +389,7 @@ def get(self, request, slug, project_id, pk=None): ) .annotate( pending_estimates=Sum( - Cast("estimate_point__value", IntegerField()), + Cast("estimate_point__value", FloatField()), filter=Q( completed_at__isnull=True, archived_at__isnull=True, @@ -278,7 +400,7 @@ def get(self, request, slug, project_id, pk=None): .order_by("first_name", "last_name") ) - assignee_distribution = ( + label_distribution = ( Issue.issue_objects.filter( issue_module__module_id=pk, workspace__slug=slug, @@ -290,12 +412,12 @@ def get(self, request, slug, project_id, pk=None): .values("label_name", "color", "label_id") .annotate( total_estimates=Sum( - Cast("estimate_point__value", IntegerField()) + Cast("estimate_point__value", FloatField()) ), ) .annotate( completed_estimates=Sum( - Cast("estimate_point__value", IntegerField()), + Cast("estimate_point__value", FloatField()), filter=Q( completed_at__isnull=False, archived_at__isnull=True, @@ -305,7 +427,7 @@ def get(self, request, slug, project_id, pk=None): ) .annotate( pending_estimates=Sum( - Cast("estimate_point__value", IntegerField()), + Cast("estimate_point__value", FloatField()), filter=Q( completed_at__isnull=True, archived_at__isnull=True, @@ -315,8 +437,8 @@ def get(self, request, slug, project_id, pk=None): ) .order_by("label_name") ) - data["estimate_distribution"]["assignee"] = assignee_distribution - data["estimate_distribution"]["label"] = label_distribution + data["estimate_distribution"]["assignees"] = assignee_distribution + data["estimate_distribution"]["labels"] = label_distribution if modules and modules.start_date and modules.target_date: data["estimate_distribution"]["completion_chart"] = ( @@ -328,6 +450,7 @@ def get(self, request, slug, project_id, pk=None): module_id=pk, ) ) + assignee_distribution = ( Issue.issue_objects.filter( issue_module__module_id=pk, @@ -353,7 +476,7 @@ def get(self, request, slug, project_id, pk=None): archived_at__isnull=True, is_draft=False, ), - ) + ), ) .annotate( completed_issues=Count( @@ -425,8 +548,6 @@ def get(self, request, slug, project_id, pk=None): "labels": label_distribution, "completion_chart": {}, } - - # Fetch the modules if modules and modules.start_date and modules.target_date: data["distribution"]["completion_chart"] = burndown_plot( queryset=modules, diff --git a/apiserver/plane/app/views/module/base.py b/apiserver/plane/app/views/module/base.py index 08420459559..0641e88a8c5 100644 --- a/apiserver/plane/app/views/module/base.py +++ b/apiserver/plane/app/views/module/base.py @@ -554,7 +554,7 @@ def retrieve(self, request, slug, project_id, pk): ) assignee_distribution = ( - Issue.objects.filter( + Issue.issue_objects.filter( issue_module__module_id=pk, workspace__slug=slug, project_id=project_id, @@ -604,7 +604,7 @@ def retrieve(self, request, slug, project_id, pk): ) label_distribution = ( - Issue.objects.filter( + Issue.issue_objects.filter( issue_module__module_id=pk, workspace__slug=slug, project_id=project_id, diff --git a/web/core/components/cycles/analytics-sidebar/issue-progress.tsx b/web/core/components/cycles/analytics-sidebar/issue-progress.tsx index 1ca239af7e7..a78974c6195 100644 --- a/web/core/components/cycles/analytics-sidebar/issue-progress.tsx +++ b/web/core/components/cycles/analytics-sidebar/issue-progress.tsx @@ -55,7 +55,7 @@ export const CycleAnalyticsProgress: FC = observer((pro const peekCycle = searchParams.get("peekCycle") || undefined; // hooks const { areEstimateEnabledByProjectId, currentActiveEstimateId, estimateById } = useProjectEstimates(); - const { getPlotTypeByCycleId, setPlotType, getCycleById, fetchCycleDetails } = useCycle(); + const { getPlotTypeByCycleId, setPlotType, getCycleById, fetchCycleDetails, fetchArchivedCycleDetails } = useCycle(); const { issuesFilter: { issueFilters, updateFilters }, } = useIssues(EIssuesStoreType.CYCLE); @@ -108,6 +108,7 @@ export const CycleAnalyticsProgress: FC = observer((pro const isCycleStartDateValid = cycleStartDate && cycleStartDate <= new Date(); const isCycleEndDateValid = cycleStartDate && cycleEndDate && cycleEndDate >= cycleStartDate; const isCycleDateValid = isCycleStartDateValid && isCycleEndDateValid; + const isArchived = !!cycleDetails?.archived_at; // handlers const onChange = async (value: TCyclePlotType) => { @@ -115,7 +116,11 @@ export const CycleAnalyticsProgress: FC = observer((pro if (!workspaceSlug || !projectId || !cycleId) return; try { setLoader(true); - await fetchCycleDetails(workspaceSlug, projectId, cycleId); + if (isArchived) { + await fetchArchivedCycleDetails(workspaceSlug, projectId, cycleId); + } else { + await fetchCycleDetails(workspaceSlug, projectId, cycleId); + } setLoader(false); } catch (error) { setLoader(false); diff --git a/web/core/components/modules/analytics-sidebar/issue-progress.tsx b/web/core/components/modules/analytics-sidebar/issue-progress.tsx index 98ac3896d76..e51e46ed4cc 100644 --- a/web/core/components/modules/analytics-sidebar/issue-progress.tsx +++ b/web/core/components/modules/analytics-sidebar/issue-progress.tsx @@ -39,7 +39,8 @@ export const ModuleAnalyticsProgress: FC = observer((p const peekModule = searchParams.get("peekModule") || undefined; // hooks const { areEstimateEnabledByProjectId, currentActiveEstimateId, estimateById } = useProjectEstimates(); - const { getPlotTypeByModuleId, setPlotType, getModuleById, fetchModuleDetails } = useModule(); + const { getPlotTypeByModuleId, setPlotType, getModuleById, fetchModuleDetails, fetchArchivedModuleDetails } = + useModule(); const { issuesFilter: { issueFilters, updateFilters }, } = useIssues(EIssuesStoreType.MODULE); @@ -92,6 +93,7 @@ export const ModuleAnalyticsProgress: FC = observer((p const isModuleStartDateValid = moduleStartDate && moduleStartDate <= new Date(); const isModuleEndDateValid = moduleStartDate && moduleEndDate && moduleEndDate >= moduleStartDate; const isModuleDateValid = isModuleStartDateValid && isModuleEndDateValid; + const isArchived = !!moduleDetails?.archived_at; // handlers const onChange = async (value: TModulePlotType) => { @@ -99,7 +101,11 @@ export const ModuleAnalyticsProgress: FC = observer((p if (!workspaceSlug || !projectId || !moduleId) return; try { setLoader(true); - await fetchModuleDetails(workspaceSlug, projectId, moduleId); + if (isArchived) { + await fetchArchivedModuleDetails(workspaceSlug, projectId, moduleId); + } else { + await fetchModuleDetails(workspaceSlug, projectId, moduleId); + } setLoader(false); } catch (error) { setLoader(false);