[WEB - 467] perf: issues listing endpoints (#3710)

* 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 <rahulramesham@gmail.com>
This commit is contained in:
Nikhil 2024-02-21 19:23:54 +05:30 committed by GitHub
parent ac6e710623
commit 370decc8aa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 669 additions and 155 deletions

View File

@ -503,9 +503,7 @@ class IssueCommentSerializer(BaseSerializer):
workspace_detail = WorkspaceLiteSerializer( workspace_detail = WorkspaceLiteSerializer(
read_only=True, source="workspace" read_only=True, source="workspace"
) )
comment_reactions = CommentReactionSerializer( comment_reactions = CommentReactionSerializer(read_only=True, many=True)
read_only=True, many=True
)
is_member = serializers.BooleanField(read_only=True) is_member = serializers.BooleanField(read_only=True)
class Meta: class Meta:
@ -558,18 +556,17 @@ class IssueStateSerializer(DynamicBaseSerializer):
class IssueSerializer(DynamicBaseSerializer): class IssueSerializer(DynamicBaseSerializer):
# ids # 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) 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 # Many to many
label_ids = serializers.PrimaryKeyRelatedField( label_ids = serializers.ListField(
read_only=True, many=True, source="labels" child=serializers.UUIDField(), required=False, allow_null=True
) )
assignee_ids = serializers.PrimaryKeyRelatedField( assignee_ids = serializers.ListField(
read_only=True, many=True, source="assignees" child=serializers.UUIDField(), required=False, allow_null=True
) )
# Count items # Count items
@ -577,9 +574,6 @@ class IssueSerializer(DynamicBaseSerializer):
attachment_count = serializers.IntegerField(read_only=True) attachment_count = serializers.IntegerField(read_only=True)
link_count = serializers.IntegerField(read_only=True) link_count = serializers.IntegerField(read_only=True)
# is_subscribed
is_subscribed = serializers.BooleanField(read_only=True)
class Meta: class Meta:
model = Issue model = Issue
fields = [ fields = [
@ -606,22 +600,19 @@ class IssueSerializer(DynamicBaseSerializer):
"updated_by", "updated_by",
"attachment_count", "attachment_count",
"link_count", "link_count",
"is_subscribed",
"is_draft", "is_draft",
"archived_at", "archived_at",
] ]
read_only_fields = fields 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): class IssueDetailSerializer(IssueSerializer):
description_html = serializers.CharField() description_html = serializers.CharField()
is_subscribed = serializers.BooleanField(read_only=True)
class Meta(IssueSerializer.Meta): class Meta(IssueSerializer.Meta):
fields = IssueSerializer.Meta.fields + ['description_html'] fields = IssueSerializer.Meta.fields + ["description_html", "is_subscribed"]
class IssueLiteSerializer(DynamicBaseSerializer): class IssueLiteSerializer(DynamicBaseSerializer):

View File

@ -90,11 +90,13 @@ urlpatterns = [
BulkImportIssuesEndpoint.as_view(), BulkImportIssuesEndpoint.as_view(),
name="project-issues-bulk", name="project-issues-bulk",
), ),
# deprecated endpoint TODO: remove once confirmed
path( path(
"workspaces/<str:slug>/my-issues/", "workspaces/<str:slug>/my-issues/",
UserWorkSpaceIssues.as_view(), UserWorkSpaceIssues.as_view(),
name="workspace-issues", name="workspace-issues",
), ),
##
path( path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/sub-issues/", "workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/sub-issues/",
SubIssuesEndpoint.as_view(), SubIssuesEndpoint.as_view(),

View File

@ -708,10 +708,11 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet):
] ]
order_by = request.GET.get("order_by", "created_at") order_by = request.GET.get("order_by", "created_at")
filters = issue_filters(request.query_params, "GET") filters = issue_filters(request.query_params, "GET")
issues = ( queryset = (
Issue.issue_objects.filter(issue_cycle__cycle_id=cycle_id) Issue.issue_objects.filter(issue_cycle__cycle_id=cycle_id)
.filter(project_id=project_id) .filter(project_id=project_id)
.filter(workspace__slug=slug) .filter(workspace__slug=slug)
.filter(**filters)
.select_related("workspace", "project", "state", "parent") .select_related("workspace", "project", "state", "parent")
.prefetch_related( .prefetch_related(
"assignees", "assignees",
@ -721,7 +722,6 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet):
) )
.order_by(order_by) .order_by(order_by)
.filter(**filters) .filter(**filters)
.annotate(module_ids=F("issue_module__module_id"))
.annotate(cycle_id=F("issue_cycle__cycle_id")) .annotate(cycle_id=F("issue_cycle__cycle_id"))
.annotate( .annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id")) link_count=IssueLink.objects.filter(issue=OuterRef("id"))
@ -745,11 +745,67 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet):
.annotate(count=Func(F("id"), function="Count")) .annotate(count=Func(F("id"), function="Count"))
.values("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( if self.fields:
issues, many=True, fields=fields if fields else None issues = IssueSerializer(
) queryset, many=True, fields=fields if fields else None
return Response(serializer.data, status=status.HTTP_200_OK) ).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): def create(self, request, slug, project_id, cycle_id):
issues = request.data.get("issues", []) issues = request.data.get("issues", [])
@ -1121,6 +1177,21 @@ class TransferCycleIssueEndpoint(BaseAPIView):
) )
.order_by("label_name") .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 serilization
label_distribution_data = [ label_distribution_data = [
{ {

View File

@ -5,6 +5,10 @@ import json
from django.utils import timezone from django.utils import timezone
from django.db.models import Q, Count, OuterRef, Func, F, Prefetch from django.db.models import Q, Count, OuterRef, Func, F, Prefetch
from django.core.serializers.json import DjangoJSONEncoder 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 # Third party imports
from rest_framework import status from rest_framework import status
@ -92,7 +96,7 @@ class InboxIssueViewSet(BaseViewSet):
Issue.objects.filter( Issue.objects.filter(
project_id=self.kwargs.get("project_id"), project_id=self.kwargs.get("project_id"),
workspace__slug=self.kwargs.get("slug"), 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") .select_related("workspace", "project", "state", "parent")
.prefetch_related("assignees", "labels", "issue_module__module") .prefetch_related("assignees", "labels", "issue_module__module")
@ -127,14 +131,75 @@ class InboxIssueViewSet(BaseViewSet):
.annotate(count=Func(F("id"), function="Count")) .annotate(count=Func(F("id"), function="Count"))
.values("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() ).distinct()
def list(self, request, slug, project_id, inbox_id): def list(self, request, slug, project_id, inbox_id):
filters = issue_filters(request.query_params, "GET") filters = issue_filters(request.query_params, "GET")
issue_queryset = self.get_queryset().filter(**filters).order_by("issue_inbox__snoozed_till", "issue_inbox__status") issue_queryset = (
issues_data = IssueSerializer(issue_queryset, expand=self.expand, many=True).data 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( return Response(
issues_data, issues,
status=status.HTTP_200_OK, status=status.HTTP_200_OK,
) )
@ -199,8 +264,8 @@ class InboxIssueViewSet(BaseViewSet):
source=request.data.get("source", "in-app"), source=request.data.get("source", "in-app"),
) )
issue = (self.get_queryset().filter(pk=issue.id).first()) issue = self.get_queryset().filter(pk=issue.id).first()
serializer = IssueSerializer(issue ,expand=self.expand) serializer = IssueSerializer(issue, expand=self.expand)
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
def partial_update(self, request, slug, project_id, inbox_id, issue_id): def partial_update(self, request, slug, project_id, inbox_id, issue_id):
@ -320,20 +385,23 @@ class InboxIssueViewSet(BaseViewSet):
if state is not None: if state is not None:
issue.state = state issue.state = state
issue.save() 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) serializer = IssueSerializer(issue, expand=self.expand)
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
return Response( return Response(
serializer.errors, status=status.HTTP_400_BAD_REQUEST serializer.errors, status=status.HTTP_400_BAD_REQUEST
) )
else: else:
issue = (self.get_queryset().filter(pk=issue_id).first()) issue = self.get_queryset().filter(pk=issue_id).first()
serializer = IssueSerializer(issue ,expand=self.expand) serializer = IssueSerializer(issue, expand=self.expand)
return Response(serializer.data, status=status.HTTP_200_OK) return Response(serializer.data, status=status.HTTP_200_OK)
def retrieve(self, request, slug, project_id, inbox_id, issue_id): def retrieve(self, request, slug, project_id, inbox_id, issue_id):
issue = self.get_queryset().filter(pk=issue_id).first() 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) return Response(serializer.data, status=status.HTTP_200_OK)
def destroy(self, request, slug, project_id, inbox_id, issue_id): def destroy(self, request, slug, project_id, inbox_id, issue_id):

View File

@ -298,12 +298,6 @@ class IssueViewSet(WebhookMixin, BaseViewSet):
.filter(workspace__slug=self.kwargs.get("slug")) .filter(workspace__slug=self.kwargs.get("slug"))
.select_related("workspace", "project", "state", "parent") .select_related("workspace", "project", "state", "parent")
.prefetch_related("assignees", "labels", "issue_module__module") .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(cycle_id=F("issue_cycle__cycle_id"))
.annotate( .annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id")) link_count=IssueLink.objects.filter(issue=OuterRef("id"))
@ -327,12 +321,40 @@ class IssueViewSet(WebhookMixin, BaseViewSet):
.annotate(count=Func(F("id"), function="Count")) .annotate(count=Func(F("id"), function="Count"))
.values("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() ).distinct()
@method_decorator(gzip_page) @method_decorator(gzip_page)
def list(self, request, slug, project_id): def list(self, request, slug, project_id):
filters = issue_filters(request.query_params, "GET") filters = issue_filters(request.query_params, "GET")
order_by_param = request.GET.get("order_by", "-created_at")
issue_queryset = self.get_queryset().filter(**filters)
# Custom ordering for priority and state # Custom ordering for priority and state
priority_order = ["urgent", "high", "medium", "low", "none"] priority_order = ["urgent", "high", "medium", "low", "none"]
state_order = [ state_order = [
@ -343,10 +365,6 @@ class IssueViewSet(WebhookMixin, BaseViewSet):
"cancelled", "cancelled",
] ]
order_by_param = request.GET.get("order_by", "-created_at")
issue_queryset = self.get_queryset().filter(**filters)
# Priority Ordering # Priority Ordering
if order_by_param == "priority" or order_by_param == "-priority": if order_by_param == "priority" or order_by_param == "-priority":
priority_order = ( priority_order = (
@ -407,9 +425,42 @@ class IssueViewSet(WebhookMixin, BaseViewSet):
else: else:
issue_queryset = issue_queryset.order_by(order_by_param) issue_queryset = issue_queryset.order_by(order_by_param)
issues = IssueSerializer( # Only use serializer when expand or fields else return by values
issue_queryset, many=True, fields=self.fields, expand=self.expand if self.expand or self.fields:
).data issues = IssueSerializer(
issue_queryset,
many=True,
fields=self.fields,
expand=self.expand,
).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, status=status.HTTP_200_OK) return Response(issues, status=status.HTTP_200_OK)
def create(self, request, slug, project_id): def create(self, request, slug, project_id):
@ -442,10 +493,38 @@ class IssueViewSet(WebhookMixin, BaseViewSet):
origin=request.META.get("HTTP_ORIGIN"), origin=request.META.get("HTTP_ORIGIN"),
) )
issue = ( issue = (
self.get_queryset().filter(pk=serializer.data["id"]).first() self.get_queryset()
.filter(pk=serializer.data["id"])
.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",
)
.first()
) )
serializer = IssueSerializer(issue) return Response(issue, status=status.HTTP_201_CREATED)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def retrieve(self, request, slug, project_id, pk=None): def retrieve(self, request, slug, project_id, pk=None):
@ -481,19 +560,45 @@ class IssueViewSet(WebhookMixin, BaseViewSet):
notification=True, notification=True,
origin=request.META.get("HTTP_ORIGIN"), origin=request.META.get("HTTP_ORIGIN"),
) )
issue = self.get_queryset().filter(pk=pk).first() issue = (
return Response( self.get_queryset()
IssueSerializer(issue).data, status=status.HTTP_200_OK .filter(pk=pk)
.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",
)
.first()
) )
return Response(issue, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def destroy(self, request, slug, project_id, pk=None): def destroy(self, request, slug, project_id, pk=None):
issue = Issue.objects.get( issue = Issue.objects.get(
workspace__slug=slug, project_id=project_id, pk=pk workspace__slug=slug, project_id=project_id, pk=pk
) )
current_instance = json.dumps(
IssueSerializer(issue).data, cls=DjangoJSONEncoder
)
issue.delete() issue.delete()
issue_activity.delay( issue_activity.delay(
type="issue.activity.deleted", type="issue.activity.deleted",
@ -501,7 +606,7 @@ class IssueViewSet(WebhookMixin, BaseViewSet):
actor_id=str(request.user.id), actor_id=str(request.user.id),
issue_id=str(pk), issue_id=str(pk),
project_id=str(project_id), project_id=str(project_id),
current_instance=current_instance, current_instance={},
epoch=int(timezone.now().timestamp()), epoch=int(timezone.now().timestamp()),
notification=True, notification=True,
origin=request.META.get("HTTP_ORIGIN"), origin=request.META.get("HTTP_ORIGIN"),
@ -509,6 +614,7 @@ class IssueViewSet(WebhookMixin, BaseViewSet):
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
# TODO: deprecated remove once confirmed
class UserWorkSpaceIssues(BaseAPIView): class UserWorkSpaceIssues(BaseAPIView):
@method_decorator(gzip_page) @method_decorator(gzip_page)
def get(self, request, slug): def get(self, request, slug):
@ -563,12 +669,6 @@ class UserWorkSpaceIssues(BaseAPIView):
.annotate(count=Func(F("id"), function="Count")) .annotate(count=Func(F("id"), function="Count"))
.values("count") .values("count")
) )
.prefetch_related(
Prefetch(
"issue_reactions",
queryset=IssueReaction.objects.select_related("actor"),
)
)
.filter(**filters) .filter(**filters)
).distinct() ).distinct()
@ -653,6 +753,7 @@ class UserWorkSpaceIssues(BaseAPIView):
return Response(issues, status=status.HTTP_200_OK) return Response(issues, status=status.HTTP_200_OK)
# TODO: deprecated remove once confirmed
class WorkSpaceIssuesEndpoint(BaseAPIView): class WorkSpaceIssuesEndpoint(BaseAPIView):
permission_classes = [ permission_classes = [
WorkSpaceAdminPermission, WorkSpaceAdminPermission,
@ -955,20 +1056,9 @@ class SubIssuesEndpoint(BaseAPIView):
Issue.issue_objects.filter( Issue.issue_objects.filter(
parent_id=issue_id, workspace__slug=slug parent_id=issue_id, workspace__slug=slug
) )
.select_related("project") .select_related("workspace", "project", "state", "parent")
.select_related("workspace") .prefetch_related("assignees", "labels", "issue_module__module")
.select_related("state") .annotate(cycle_id=F("issue_cycle__cycle_id"))
.select_related("parent")
.prefetch_related("assignees")
.prefetch_related("labels")
.annotate(
sub_issues_count=Issue.issue_objects.filter(
parent=OuterRef("id")
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate( .annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id")) link_count=IssueLink.objects.filter(issue=OuterRef("id"))
.order_by() .order_by()
@ -983,11 +1073,39 @@ class SubIssuesEndpoint(BaseAPIView):
.annotate(count=Func(F("id"), function="Count")) .annotate(count=Func(F("id"), function="Count"))
.values("count") .values("count")
) )
.prefetch_related( .annotate(
Prefetch( sub_issues_count=Issue.issue_objects.filter(
"issue_reactions", parent=OuterRef("id")
queryset=IssueReaction.objects.select_related("actor"),
) )
.order_by()
.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())),
),
) )
.annotate(state_group=F("state__group")) .annotate(state_group=F("state__group"))
) )
@ -997,13 +1115,36 @@ class SubIssuesEndpoint(BaseAPIView):
for sub_issue in sub_issues: for sub_issue in sub_issues:
result[sub_issue.state_group].append(str(sub_issue.id)) result[sub_issue.state_group].append(str(sub_issue.id))
serializer = IssueSerializer( sub_issues = sub_issues.values(
sub_issues, "id",
many=True, "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( return Response(
{ {
"sub_issues": serializer.data, "sub_issues": sub_issues,
"state_distribution": result, "state_distribution": result,
}, },
status=status.HTTP_200_OK, status=status.HTTP_200_OK,
@ -1291,15 +1432,36 @@ class IssueArchiveViewSet(BaseViewSet):
.annotate(count=Func(F("id"), function="Count")) .annotate(count=Func(F("id"), function="Count"))
.values("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())),
),
)
) )
@method_decorator(gzip_page) @method_decorator(gzip_page)
def list(self, request, slug, project_id): def list(self, request, slug, project_id):
fields = [
field
for field in request.GET.get("fields", "").split(",")
if field
]
filters = issue_filters(request.query_params, "GET") filters = issue_filters(request.query_params, "GET")
show_sub_issues = request.GET.get("show_sub_issues", "true") show_sub_issues = request.GET.get("show_sub_issues", "true")
@ -1382,10 +1544,40 @@ class IssueArchiveViewSet(BaseViewSet):
if show_sub_issues == "true" if show_sub_issues == "true"
else issue_queryset.filter(parent__isnull=True) else issue_queryset.filter(parent__isnull=True)
) )
if self.expand or self.fields:
issues = IssueSerializer( issues = IssueSerializer(
issue_queryset, many=True, fields=fields if fields else None issue_queryset,
).data many=True,
fields=self.fields,
).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, status=status.HTTP_200_OK) return Response(issues, status=status.HTTP_200_OK)
def retrieve(self, request, slug, project_id, pk=None): def retrieve(self, request, slug, project_id, pk=None):
@ -1856,12 +2048,6 @@ class IssueDraftViewSet(BaseViewSet):
.filter(is_draft=True) .filter(is_draft=True)
.select_related("workspace", "project", "state", "parent") .select_related("workspace", "project", "state", "parent")
.prefetch_related("assignees", "labels", "issue_module__module") .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(cycle_id=F("issue_cycle__cycle_id"))
.annotate( .annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id")) link_count=IssueLink.objects.filter(issue=OuterRef("id"))
@ -1885,6 +2071,32 @@ class IssueDraftViewSet(BaseViewSet):
.annotate(count=Func(F("id"), function="Count")) .annotate(count=Func(F("id"), function="Count"))
.values("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() ).distinct()
@method_decorator(gzip_page) @method_decorator(gzip_page)
@ -1970,9 +2182,42 @@ class IssueDraftViewSet(BaseViewSet):
else: else:
issue_queryset = issue_queryset.order_by(order_by_param) issue_queryset = issue_queryset.order_by(order_by_param)
issues = IssueSerializer( # Only use serializer when expand else return by values
issue_queryset, many=True, fields=fields if fields else None if self.expand or self.fields:
).data issues = IssueSerializer(
issue_queryset,
many=True,
fields=self.fields,
expand=self.expand,
).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, status=status.HTTP_200_OK) return Response(issues, status=status.HTTP_200_OK)
def create(self, request, slug, project_id): def create(self, request, slug, project_id):
@ -2057,9 +2302,6 @@ class IssueDraftViewSet(BaseViewSet):
issue = Issue.objects.get( issue = Issue.objects.get(
workspace__slug=slug, project_id=project_id, pk=pk workspace__slug=slug, project_id=project_id, pk=pk
) )
current_instance = json.dumps(
IssueSerializer(issue).data, cls=DjangoJSONEncoder
)
issue.delete() issue.delete()
issue_activity.delay( issue_activity.delay(
type="issue_draft.activity.deleted", type="issue_draft.activity.deleted",
@ -2067,7 +2309,7 @@ class IssueDraftViewSet(BaseViewSet):
actor_id=str(request.user.id), actor_id=str(request.user.id),
issue_id=str(pk), issue_id=str(pk),
project_id=str(project_id), project_id=str(project_id),
current_instance=current_instance, current_instance={},
epoch=int(timezone.now().timestamp()), epoch=int(timezone.now().timestamp()),
notification=True, notification=True,
origin=request.META.get("HTTP_ORIGIN"), origin=request.META.get("HTTP_ORIGIN"),

View File

@ -449,8 +449,7 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet):
issue_module__module_id=self.kwargs.get("module_id"), issue_module__module_id=self.kwargs.get("module_id"),
) )
.select_related("workspace", "project", "state", "parent") .select_related("workspace", "project", "state", "parent")
.prefetch_related("labels", "assignees") .prefetch_related("assignees", "labels", "issue_module__module")
.prefetch_related("issue_module__module")
.annotate(cycle_id=F("issue_cycle__cycle_id")) .annotate(cycle_id=F("issue_cycle__cycle_id"))
.annotate( .annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id")) link_count=IssueLink.objects.filter(issue=OuterRef("id"))
@ -474,6 +473,32 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet):
.annotate(count=Func(F("id"), function="Count")) .annotate(count=Func(F("id"), function="Count"))
.values("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() ).distinct()
@method_decorator(gzip_page) @method_decorator(gzip_page)
@ -485,10 +510,39 @@ class ModuleIssueViewSet(WebhookMixin, BaseViewSet):
] ]
filters = issue_filters(request.query_params, "GET") filters = issue_filters(request.query_params, "GET")
issue_queryset = self.get_queryset().filter(**filters) issue_queryset = self.get_queryset().filter(**filters)
serializer = IssueSerializer( if self.fields or self.expand:
issue_queryset, many=True, fields=fields if fields else None issues = IssueSerializer(
) issue_queryset, many=True, fields=fields if fields else None
return Response(serializer.data, status=status.HTTP_200_OK) ).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, status=status.HTTP_200_OK)
# create multiple issues inside a module # create multiple issues inside a module
def create_module_issues(self, request, slug, project_id, module_id): def create_module_issues(self, request, slug, project_id, module_id):

View File

@ -1,6 +1,6 @@
# Django imports # Django imports
from django.db.models import ( from django.db.models import (
Prefetch, Q,
OuterRef, OuterRef,
Func, Func,
F, F,
@ -13,16 +13,21 @@ from django.db.models import (
) )
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.views.decorators.gzip import gzip_page from django.views.decorators.gzip import gzip_page
from django.db.models import Prefetch, OuterRef, Exists 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
from django.contrib.postgres.aggregates import ArrayAgg
from django.contrib.postgres.fields import ArrayField
from django.db.models import Value, UUIDField
# Third party imports # Third party imports
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework import status from rest_framework import status
# Module imports # Module imports
from . import BaseViewSet, BaseAPIView from . import BaseViewSet
from plane.app.serializers import ( from plane.app.serializers import (
GlobalViewSerializer,
IssueViewSerializer, IssueViewSerializer,
IssueSerializer, IssueSerializer,
IssueViewFavoriteSerializer, IssueViewFavoriteSerializer,
@ -30,22 +35,16 @@ from plane.app.serializers import (
from plane.app.permissions import ( from plane.app.permissions import (
WorkspaceEntityPermission, WorkspaceEntityPermission,
ProjectEntityPermission, ProjectEntityPermission,
WorkspaceViewerPermission,
ProjectLitePermission,
) )
from plane.db.models import ( from plane.db.models import (
Workspace, Workspace,
GlobalView,
IssueView, IssueView,
Issue, Issue,
IssueViewFavorite, IssueViewFavorite,
IssueReaction,
IssueLink, IssueLink,
IssueAttachment, IssueAttachment,
IssueSubscriber,
) )
from plane.utils.issue_filters import issue_filters from plane.utils.issue_filters import issue_filters
from plane.utils.grouper import group_results
class GlobalViewViewSet(BaseViewSet): class GlobalViewViewSet(BaseViewSet):
@ -89,11 +88,54 @@ class GlobalViewIssuesViewSet(BaseViewSet):
.filter(workspace__slug=self.kwargs.get("slug")) .filter(workspace__slug=self.kwargs.get("slug"))
.select_related("workspace", "project", "state", "parent") .select_related("workspace", "project", "state", "parent")
.prefetch_related("assignees", "labels", "issue_module__module") .prefetch_related("assignees", "labels", "issue_module__module")
.prefetch_related( .annotate(cycle_id=F("issue_cycle__cycle_id"))
Prefetch( .annotate(
"issue_reactions", link_count=IssueLink.objects.filter(issue=OuterRef("id"))
queryset=IssueReaction.objects.select_related("actor"), .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")
)
.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())),
),
) )
) )
@ -123,28 +165,6 @@ class GlobalViewIssuesViewSet(BaseViewSet):
.filter(**filters) .filter(**filters)
.filter(project__project_projectmember__member=self.request.user) .filter(project__project_projectmember__member=self.request.user)
.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()
.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")
)
) )
# Priority Ordering # Priority Ordering
@ -207,10 +227,39 @@ class GlobalViewIssuesViewSet(BaseViewSet):
else: else:
issue_queryset = issue_queryset.order_by(order_by_param) issue_queryset = issue_queryset.order_by(order_by_param)
serializer = IssueSerializer( if self.fields:
issue_queryset, many=True, fields=fields if fields else None issues = IssueSerializer(
) issue_queryset, many=True, fields=self.fields
return Response(serializer.data, status=status.HTTP_200_OK) ).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, status=status.HTTP_200_OK)
class IssueViewViewSet(BaseViewSet): class IssueViewViewSet(BaseViewSet):

View File

@ -26,6 +26,10 @@ from django.db.models import (
) )
from django.db.models.functions import ExtractWeek, Cast, ExtractDay from django.db.models.functions import ExtractWeek, Cast, ExtractDay
from django.db.models.fields import DateField from django.db.models.fields import DateField
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 modules # Third party modules
from rest_framework import status from rest_framework import status
@ -1380,6 +1384,32 @@ class WorkspaceUserProfileIssuesEndpoint(BaseAPIView):
.annotate(count=Func(F("id"), function="Count")) .annotate(count=Func(F("id"), function="Count"))
.values("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("created_at") .order_by("created_at")
).distinct() ).distinct()

View File

@ -48,6 +48,13 @@ export const IssueSubscription: FC<TIssueSubscription> = observer((props) => {
} }
}; };
if (!subscription)
return (
<Loader>
<Loader.Item width="92px" height="27px" />
</Loader>
);
return ( return (
<> <>
{subscription ? ( {subscription ? (