chore: add labels data in cycles (#1223)

* dev: add labels data for all cycles

* dev: add assignees and labels percentage

* dev: initial peice on cycle burn down chart

* dev: cycles burn down chat
This commit is contained in:
pablohashescobar 2023-06-16 18:57:49 +05:30 committed by GitHub
parent e9a0eb87cc
commit 6b1d20449b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 237 additions and 11 deletions

View File

@ -1,3 +1,6 @@
# Django imports
from django.db.models.functions import TruncDate
# Third party imports # Third party imports
from rest_framework import serializers from rest_framework import serializers
@ -20,13 +23,13 @@ class CycleSerializer(BaseSerializer):
unstarted_issues = serializers.IntegerField(read_only=True) unstarted_issues = serializers.IntegerField(read_only=True)
backlog_issues = serializers.IntegerField(read_only=True) backlog_issues = serializers.IntegerField(read_only=True)
assignees = serializers.SerializerMethodField(read_only=True) assignees = serializers.SerializerMethodField(read_only=True)
labels = serializers.SerializerMethodField(read_only=True)
total_estimates = serializers.IntegerField(read_only=True) total_estimates = serializers.IntegerField(read_only=True)
completed_estimates = serializers.IntegerField(read_only=True) completed_estimates = serializers.IntegerField(read_only=True)
started_estimates = serializers.IntegerField(read_only=True) started_estimates = serializers.IntegerField(read_only=True)
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace") workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
project_detail = ProjectLiteSerializer(read_only=True, source="project") project_detail = ProjectLiteSerializer(read_only=True, source="project")
def get_assignees(self, obj): def get_assignees(self, obj):
members = [ members = [
{ {
@ -44,6 +47,24 @@ class CycleSerializer(BaseSerializer):
unique_list = [dict(item) for item in unique_objects] unique_list = [dict(item) for item in unique_objects]
return unique_list return unique_list
def get_labels(self, obj):
labels = [
{
"name": label.name,
"color": label.color,
"id": label.id,
}
for issue_cycle in obj.issue_cycle.all()
for label in issue_cycle.issue.labels.all()
]
# Use a set comprehension to return only the unique objects
unique_objects = {frozenset(item.items()) for item in labels}
# Convert the set back to a list of dictionaries
unique_list = [dict(item) for item in unique_objects]
return unique_list
class Meta: class Meta:
model = Cycle model = Cycle

View File

@ -1,5 +1,6 @@
# Python imports # Python imports
import json import json
from datetime import datetime, timedelta
# Django imports # Django imports
from django.db import IntegrityError from django.db import IntegrityError
@ -14,6 +15,7 @@ from django.db.models import (
Prefetch, Prefetch,
Sum, Sum,
) )
from django.db.models.functions import TruncDate
from django.core import serializers from django.core import serializers
from django.utils import timezone from django.utils import timezone
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
@ -41,10 +43,12 @@ from plane.db.models import (
CycleFavorite, CycleFavorite,
IssueLink, IssueLink,
IssueAttachment, IssueAttachment,
Label,
) )
from plane.bgtasks.issue_activites_task import issue_activity from plane.bgtasks.issue_activites_task import issue_activity
from plane.utils.grouper import group_results from plane.utils.grouper import group_results
from plane.utils.issue_filters import issue_filters from plane.utils.issue_filters import issue_filters
from plane.utils.analytics_plot import burndown_plot
class CycleViewSet(BaseViewSet): class CycleViewSet(BaseViewSet):
@ -148,12 +152,18 @@ class CycleViewSet(BaseViewSet):
queryset=User.objects.only("avatar", "first_name", "id").distinct(), queryset=User.objects.only("avatar", "first_name", "id").distinct(),
) )
) )
.prefetch_related(
Prefetch(
"issue_cycle__issue__labels",
queryset=Label.objects.only("name", "color", "id").distinct(),
)
)
.order_by("-is_favorite", "name") .order_by("-is_favorite", "name")
.distinct() .distinct()
) )
def list(self, request, slug, project_id): def list(self, request, slug, project_id):
try: # try:
queryset = self.get_queryset() queryset = self.get_queryset()
cycle_view = request.GET.get("cycle_view", False) cycle_view = request.GET.get("cycle_view", False)
if not cycle_view: if not cycle_view:
@ -167,15 +177,83 @@ class CycleViewSet(BaseViewSet):
return Response( return Response(
CycleSerializer(queryset, many=True).data, status=status.HTTP_200_OK CycleSerializer(queryset, many=True).data, status=status.HTTP_200_OK
) )
# Current Cycle # Current Cycle
if cycle_view == "current": if cycle_view == "current":
queryset = queryset.filter( queryset = queryset.filter(
start_date__lte=timezone.now(), start_date__lte=timezone.now(),
end_date__gte=timezone.now(), end_date__gte=timezone.now(),
) )
data = CycleSerializer(queryset, many=True).data
if len(data):
assignee_distribution = (
Issue.objects.filter(
issue_cycle__cycle_id=data[0]["id"],
workspace__slug=slug,
project_id=project_id,
)
.annotate(first_name=F("assignees__first_name"))
.annotate(last_name=F("assignees__last_name"))
.annotate(assignee_id=F("assignees__id"))
.values("first_name", "last_name", "assignee_id")
.annotate(total_issues=Count("assignee_id"))
.annotate(
completed_issues=Count(
"assignee_id",
filter=Q(completed_at__isnull=False),
)
)
.annotate(
pending_issues=Count(
"assignee_id",
filter=Q(completed_at__isnull=True),
)
)
.order_by("first_name", "last_name")
)
label_distribution = (
Issue.objects.filter(
issue_cycle__cycle_id=data[0]["id"],
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_issues=Count("label_id"))
.annotate(
completed_issues=Count(
"label_id",
filter=Q(completed_at__isnull=False),
)
)
.annotate(
pending_issues=Count(
"label_id",
filter=Q(completed_at__isnull=True),
)
)
.order_by("label_name")
)
data[0]["distribution"] = {
"assignees": assignee_distribution,
"labels": label_distribution,
"completion_chart": {},
}
if data[0]["start_date"] and data[0]["end_date"]:
data[0]["distribution"]["completion_chart"] = burndown_plot(
queryset=queryset.first(),
slug=slug,
project_id=project_id,
cycle_id=data[0]["id"],
)
return Response( return Response(
CycleSerializer(queryset, many=True).data, status=status.HTTP_200_OK data, status=status.HTTP_200_OK
) )
# Upcoming Cycles # Upcoming Cycles
@ -198,6 +276,7 @@ class CycleViewSet(BaseViewSet):
end_date=None, end_date=None,
start_date=None, start_date=None,
) )
return Response( return Response(
CycleSerializer(queryset, many=True).data, status=status.HTTP_200_OK CycleSerializer(queryset, many=True).data, status=status.HTTP_200_OK
) )
@ -214,12 +293,12 @@ class CycleViewSet(BaseViewSet):
return Response( return Response(
{"error": "No matching view found"}, status=status.HTTP_400_BAD_REQUEST {"error": "No matching view found"}, status=status.HTTP_400_BAD_REQUEST
) )
except Exception as e: # except Exception as e:
capture_exception(e) # print(e)
return Response( # return Response(
{"error": "Something went wrong please try again later"}, # {"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST, # status=status.HTTP_400_BAD_REQUEST,
) # )
def create(self, request, slug, project_id): def create(self, request, slug, project_id):
try: try:
@ -282,6 +361,86 @@ class CycleViewSet(BaseViewSet):
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
def retrieve(self, request, slug, project_id, pk):
try:
queryset = self.get_queryset().get(pk=pk)
assignee_distribution = (
Issue.objects.filter(
issue_cycle__cycle_id=pk,
workspace__slug=slug,
project_id=project_id,
)
.annotate(first_name=F("assignees__first_name"))
.annotate(last_name=F("assignees__last_name"))
.annotate(assignee_id=F("assignees__id"))
.values("first_name", "last_name", "assignee_id")
.annotate(total_issues=Count("assignee_id"))
.annotate(
completed_issues=Count(
"assignee_id",
filter=Q(completed_at__isnull=False),
)
)
.annotate(
pending_issues=Count(
"assignee_id",
filter=Q(completed_at__isnull=True),
)
)
.order_by("first_name", "last_name")
)
label_distribution = (
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_issues=Count("label_id"))
.annotate(
completed_issues=Count(
"label_id",
filter=Q(completed_at__isnull=False),
)
)
.annotate(
pending_issues=Count(
"label_id",
filter=Q(completed_at__isnull=True),
)
)
.order_by("label_name")
)
data = CycleSerializer(queryset).data
data["distribution"] = {
"assignees": assignee_distribution,
"labels": label_distribution,
"completion_chart": {},
}
if queryset.start_date and queryset.end_date:
data["distribution"]["completion_chart"] = burndown_plot(
queryset=queryset, slug=slug, project_id=project_id, cycle_id=pk
)
return Response(
data,
status=status.HTTP_200_OK,
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class CycleIssueViewSet(BaseViewSet): class CycleIssueViewSet(BaseViewSet):
serializer_class = CycleIssueSerializer serializer_class = CycleIssueSerializer

View File

@ -1,11 +1,16 @@
# Python imports # Python imports
from itertools import groupby from itertools import groupby
from datetime import timedelta
# Django import # Django import
from django.db import models from django.db import models
from django.db.models.functions import TruncDate
from django.db.models import Count, F, Sum, Value, Case, When, CharField from django.db.models import Count, F, Sum, Value, Case, When, CharField
from django.db.models.functions import Coalesce, ExtractMonth, ExtractYear, Concat from django.db.models.functions import Coalesce, ExtractMonth, ExtractYear, Concat
# Module imports
from plane.db.models import Issue
def build_graph_plot(queryset, x_axis, y_axis, segment=None): def build_graph_plot(queryset, x_axis, y_axis, segment=None):
@ -74,3 +79,44 @@ def build_graph_plot(queryset, x_axis, y_axis, segment=None):
else: else:
sorted_data = dict(sorted(grouped_data.items(), key=lambda x: (x[0] == "None", x[0]))) sorted_data = dict(sorted(grouped_data.items(), key=lambda x: (x[0] == "None", x[0])))
return sorted_data return sorted_data
def burndown_plot(queryset, slug, project_id, cycle_id):
# Get all dates between the two dates
date_range = [
queryset.start_date + timedelta(days=x)
for x in range((queryset.end_date - queryset.start_date).days + 1)
]
chart_data = {str(date): 0 for date in date_range}
# Total Issues in Cycle
total_issues = queryset.total_issues
completed_issues_distribution = (
Issue.objects.filter(
workspace__slug=slug,
project_id=project_id,
issue_cycle__cycle_id=cycle_id,
)
.annotate(date=TruncDate("completed_at"))
.values("date")
.annotate(total_completed=Count("id"))
.values("date", "total_completed")
.order_by("date")
)
for date in date_range:
cumulative_pending_issues = total_issues
total_completed = 0
total_completed = sum(
[
item["total_completed"]
for item in completed_issues_distribution
if item["date"] is not None and item["date"] <= date
]
)
cumulative_pending_issues -= total_completed
chart_data[str(date)] = cumulative_pending_issues
return chart_data