Skip to content

Commit

Permalink
[WEB - 467] perf: issues listing endpoints (#3710)
Browse files Browse the repository at this point in the history
* dev: update issue listing apis

* dev: optimize issue listing apis

* fix: sub issues endpoint

* add loading state for subscription in issueDetail

* fix: import error

---------

Co-authored-by: rahulramesha <[email protected]>
  • Loading branch information
pablohashescobar and rahulramesha authored Feb 21, 2024
1 parent ac6e710 commit 370decc
Show file tree
Hide file tree
Showing 9 changed files with 668 additions and 154 deletions.
31 changes: 11 additions & 20 deletions apiserver/plane/app/serializers/issue.py
Original file line number Diff line number Diff line change
Expand Up @@ -503,9 +503,7 @@ class IssueCommentSerializer(BaseSerializer):
workspace_detail = WorkspaceLiteSerializer(
read_only=True, source="workspace"
)
comment_reactions = CommentReactionSerializer(
read_only=True, many=True
)
comment_reactions = CommentReactionSerializer(read_only=True, many=True)
is_member = serializers.BooleanField(read_only=True)

class Meta:
Expand Down Expand Up @@ -558,28 +556,24 @@ class Meta:

class IssueSerializer(DynamicBaseSerializer):
# ids
project_id = serializers.PrimaryKeyRelatedField(read_only=True)
state_id = serializers.PrimaryKeyRelatedField(read_only=True)
parent_id = serializers.PrimaryKeyRelatedField(read_only=True)
cycle_id = serializers.PrimaryKeyRelatedField(read_only=True)
module_ids = serializers.SerializerMethodField()
module_ids = serializers.ListField(
child=serializers.UUIDField(), required=False, allow_null=True
)

# Many to many
label_ids = serializers.PrimaryKeyRelatedField(
read_only=True, many=True, source="labels"
label_ids = serializers.ListField(
child=serializers.UUIDField(), required=False, allow_null=True
)
assignee_ids = serializers.PrimaryKeyRelatedField(
read_only=True, many=True, source="assignees"
assignee_ids = serializers.ListField(
child=serializers.UUIDField(), required=False, allow_null=True
)

# Count items
sub_issues_count = serializers.IntegerField(read_only=True)
attachment_count = serializers.IntegerField(read_only=True)
link_count = serializers.IntegerField(read_only=True)

# is_subscribed
is_subscribed = serializers.BooleanField(read_only=True)

class Meta:
model = Issue
fields = [
Expand All @@ -606,22 +600,19 @@ class Meta:
"updated_by",
"attachment_count",
"link_count",
"is_subscribed",
"is_draft",
"archived_at",
]
read_only_fields = fields

def get_module_ids(self, obj):
# Access the prefetched modules and extract module IDs
return [module for module in obj.issue_module.values_list("module_id", flat=True)]


class IssueDetailSerializer(IssueSerializer):
description_html = serializers.CharField()
description_html = serializers.CharField()
is_subscribed = serializers.BooleanField(read_only=True)

class Meta(IssueSerializer.Meta):
fields = IssueSerializer.Meta.fields + ['description_html']
fields = IssueSerializer.Meta.fields + ["description_html", "is_subscribed"]


class IssueLiteSerializer(DynamicBaseSerializer):
Expand Down
2 changes: 2 additions & 0 deletions apiserver/plane/app/urls/issue.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,11 +90,13 @@
BulkImportIssuesEndpoint.as_view(),
name="project-issues-bulk",
),
# deprecated endpoint TODO: remove once confirmed
path(
"workspaces/<str:slug>/my-issues/",
UserWorkSpaceIssues.as_view(),
name="workspace-issues",
),
##
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/sub-issues/",
SubIssuesEndpoint.as_view(),
Expand Down
83 changes: 77 additions & 6 deletions apiserver/plane/app/views/cycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -708,10 +708,11 @@ def list(self, request, slug, project_id, cycle_id):
]
order_by = request.GET.get("order_by", "created_at")
filters = issue_filters(request.query_params, "GET")
issues = (
queryset = (
Issue.issue_objects.filter(issue_cycle__cycle_id=cycle_id)
.filter(project_id=project_id)
.filter(workspace__slug=slug)
.filter(**filters)
.select_related("workspace", "project", "state", "parent")
.prefetch_related(
"assignees",
Expand All @@ -721,7 +722,6 @@ def list(self, request, slug, project_id, cycle_id):
)
.order_by(order_by)
.filter(**filters)
.annotate(module_ids=F("issue_module__module_id"))
.annotate(cycle_id=F("issue_cycle__cycle_id"))
.annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
Expand All @@ -745,11 +745,67 @@ def list(self, request, slug, project_id, cycle_id):
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
label_ids=Coalesce(
ArrayAgg(
"labels__id",
distinct=True,
filter=~Q(labels__id__isnull=True),
),
Value([], output_field=ArrayField(UUIDField())),
),
assignee_ids=Coalesce(
ArrayAgg(
"assignees__id",
distinct=True,
filter=~Q(assignees__id__isnull=True),
),
Value([], output_field=ArrayField(UUIDField())),
),
module_ids=Coalesce(
ArrayAgg(
"issue_module__module_id",
distinct=True,
filter=~Q(issue_module__module_id__isnull=True),
),
Value([], output_field=ArrayField(UUIDField())),
),
)
.order_by(order_by)
)
serializer = IssueSerializer(
issues, many=True, fields=fields if fields else None
)
return Response(serializer.data, status=status.HTTP_200_OK)
if self.fields:
issues = IssueSerializer(
queryset, many=True, fields=fields if fields else None
).data
else:
issues = queryset.values(
"id",
"name",
"state_id",
"sort_order",
"completed_at",
"estimate_point",
"priority",
"start_date",
"target_date",
"sequence_id",
"project_id",
"parent_id",
"cycle_id",
"module_ids",
"label_ids",
"assignee_ids",
"sub_issues_count",
"created_at",
"updated_at",
"created_by",
"updated_by",
"attachment_count",
"link_count",
"is_draft",
"archived_at",
)
return Response(issues, status=status.HTTP_200_OK)

def create(self, request, slug, project_id, cycle_id):
issues = request.data.get("issues", [])
Expand Down Expand Up @@ -1121,6 +1177,21 @@ def post(self, request, slug, project_id, cycle_id):
)
.order_by("label_name")
)

assignee_distribution_data = [
{
"display_name": item["display_name"],
"assignee_id": (
str(item["assignee_id"]) if item["assignee_id"] else None
),
"avatar": item["avatar"],
"total_issues": item["total_issues"],
"completed_issues": item["completed_issues"],
"pending_issues": item["pending_issues"],
}
for item in assignee_distribution
]

# Label distribution serilization
label_distribution_data = [
{
Expand Down
88 changes: 78 additions & 10 deletions apiserver/plane/app/views/inbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
from django.utils import timezone
from django.db.models import Q, Count, OuterRef, Func, F, Prefetch
from django.core.serializers.json import DjangoJSONEncoder
from django.contrib.postgres.aggregates import ArrayAgg
from django.contrib.postgres.fields import ArrayField
from django.db.models import Value, UUIDField
from django.db.models.functions import Coalesce

# Third party imports
from rest_framework import status
Expand Down Expand Up @@ -92,7 +96,7 @@ def get_queryset(self):
Issue.objects.filter(
project_id=self.kwargs.get("project_id"),
workspace__slug=self.kwargs.get("slug"),
issue_inbox__inbox_id=self.kwargs.get("inbox_id")
issue_inbox__inbox_id=self.kwargs.get("inbox_id"),
)
.select_related("workspace", "project", "state", "parent")
.prefetch_related("assignees", "labels", "issue_module__module")
Expand Down Expand Up @@ -127,14 +131,75 @@ def get_queryset(self):
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
label_ids=Coalesce(
ArrayAgg(
"labels__id",
distinct=True,
filter=~Q(labels__id__isnull=True),
),
Value([], output_field=ArrayField(UUIDField())),
),
assignee_ids=Coalesce(
ArrayAgg(
"assignees__id",
distinct=True,
filter=~Q(assignees__id__isnull=True),
),
Value([], output_field=ArrayField(UUIDField())),
),
module_ids=Coalesce(
ArrayAgg(
"issue_module__module_id",
distinct=True,
filter=~Q(issue_module__module_id__isnull=True),
),
Value([], output_field=ArrayField(UUIDField())),
),
)
).distinct()

def list(self, request, slug, project_id, inbox_id):
filters = issue_filters(request.query_params, "GET")
issue_queryset = self.get_queryset().filter(**filters).order_by("issue_inbox__snoozed_till", "issue_inbox__status")
issues_data = IssueSerializer(issue_queryset, expand=self.expand, many=True).data
issue_queryset = (
self.get_queryset()
.filter(**filters)
.order_by("issue_inbox__snoozed_till", "issue_inbox__status")
)
if self.expand:
issues = IssueSerializer(
issue_queryset, expand=self.expand, many=True
).data
else:
issues = issue_queryset.values(
"id",
"name",
"state_id",
"sort_order",
"completed_at",
"estimate_point",
"priority",
"start_date",
"target_date",
"sequence_id",
"project_id",
"parent_id",
"cycle_id",
"module_ids",
"label_ids",
"assignee_ids",
"sub_issues_count",
"created_at",
"updated_at",
"created_by",
"updated_by",
"attachment_count",
"link_count",
"is_draft",
"archived_at",
)
return Response(
issues_data,
issues,
status=status.HTTP_200_OK,
)

Expand Down Expand Up @@ -199,8 +264,8 @@ def create(self, request, slug, project_id, inbox_id):
source=request.data.get("source", "in-app"),
)

issue = (self.get_queryset().filter(pk=issue.id).first())
serializer = IssueSerializer(issue ,expand=self.expand)
issue = self.get_queryset().filter(pk=issue.id).first()
serializer = IssueSerializer(issue, expand=self.expand)
return Response(serializer.data, status=status.HTTP_200_OK)

def partial_update(self, request, slug, project_id, inbox_id, issue_id):
Expand Down Expand Up @@ -320,20 +385,23 @@ def partial_update(self, request, slug, project_id, inbox_id, issue_id):
if state is not None:
issue.state = state
issue.save()
issue = (self.get_queryset().filter(pk=issue_id).first())
issue = self.get_queryset().filter(pk=issue_id).first()
serializer = IssueSerializer(issue, expand=self.expand)
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(
serializer.errors, status=status.HTTP_400_BAD_REQUEST
)
else:
issue = (self.get_queryset().filter(pk=issue_id).first())
serializer = IssueSerializer(issue ,expand=self.expand)
issue = self.get_queryset().filter(pk=issue_id).first()
serializer = IssueSerializer(issue, expand=self.expand)
return Response(serializer.data, status=status.HTTP_200_OK)

def retrieve(self, request, slug, project_id, inbox_id, issue_id):
issue = self.get_queryset().filter(pk=issue_id).first()
serializer = IssueDetailSerializer(issue, expand=self.expand,)
serializer = IssueDetailSerializer(
issue,
expand=self.expand,
)
return Response(serializer.data, status=status.HTTP_200_OK)

def destroy(self, request, slug, project_id, inbox_id, issue_id):
Expand Down
Loading

0 comments on commit 370decc

Please sign in to comment.