diff --git a/apiserver/plane/app/serializers/issue.py b/apiserver/plane/app/serializers/issue.py index 34f000067..8bbc22a9f 100644 --- a/apiserver/plane/app/serializers/issue.py +++ b/apiserver/plane/app/serializers/issue.py @@ -614,10 +614,11 @@ class IssueSerializer(DynamicBaseSerializer): class IssueDetailSerializer(IssueSerializer): - description_html = serializers.CharField() + description_html = serializers.CharField() + is_subscribed = serializers.BooleanField() class Meta(IssueSerializer.Meta): - fields = IssueSerializer.Meta.fields + ['description_html'] + fields = IssueSerializer.Meta.fields + ['description_html', 'is_subscribed'] class IssueLiteSerializer(DynamicBaseSerializer): diff --git a/apiserver/plane/app/views/inbox.py b/apiserver/plane/app/views/inbox.py index 01eee78e3..e503591c0 100644 --- a/apiserver/plane/app/views/inbox.py +++ b/apiserver/plane/app/views/inbox.py @@ -3,7 +3,7 @@ import json # Django import 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, Exists from django.core.serializers.json import DjangoJSONEncoder # Third party imports @@ -21,6 +21,7 @@ from plane.db.models import ( IssueLink, IssueAttachment, ProjectMember, + IssueSubscriber, ) from plane.app.serializers import ( IssueSerializer, @@ -92,7 +93,7 @@ class InboxIssueViewSet(BaseViewSet): 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") @@ -131,8 +132,14 @@ class InboxIssueViewSet(BaseViewSet): 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") + ) + issues_data = IssueSerializer( + issue_queryset, expand=self.expand, many=True + ).data return Response( issues_data, status=status.HTTP_200_OK, @@ -199,8 +206,8 @@ class InboxIssueViewSet(BaseViewSet): 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): @@ -320,20 +327,34 @@ class InboxIssueViewSet(BaseViewSet): 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 = IssueSerializer(issue, expand=self.expand,) + issue = ( + self.get_queryset() + .filter(pk=issue_id) + .annotate( + is_subscribed=Exists( + IssueSubscriber.objects.filter( + subscriber_id=request.user.id, issue_id=issue_id + ) + ) + ) + .first() + ) + serializer = IssueSerializer( + issue, + expand=self.expand, + ) return Response(serializer.data, status=status.HTTP_200_OK) def destroy(self, request, slug, project_id, inbox_id, issue_id): diff --git a/apiserver/plane/app/views/issue.py b/apiserver/plane/app/views/issue.py index a7d64e7e4..8eaf23d05 100644 --- a/apiserver/plane/app/views/issue.py +++ b/apiserver/plane/app/views/issue.py @@ -267,7 +267,18 @@ class IssueViewSet(WebhookMixin, BaseViewSet): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) def retrieve(self, request, slug, project_id, pk=None): - issue = self.get_queryset().filter(pk=pk).first() + issue = ( + self.get_queryset() + .filter(pk=pk) + .annotate( + is_subscribed=Exists( + IssueSubscriber.objects.filter( + subscriber_id=request.user.id, issue_id=pk + ) + ) + ) + .first() + ) return Response( IssueDetailSerializer( issue, fields=self.fields, expand=self.expand @@ -715,7 +726,9 @@ class LabelViewSet(BaseViewSet): ) @invalidate_path_cache("/api/workspaces/:slug/labels/") - @invalidate_path_cache("/api/workspaces/:slug/projects/:project_id/issue-labels/") + @invalidate_path_cache( + "/api/workspaces/:slug/projects/:project_id/issue-labels/" + ) def create(self, request, slug, project_id): try: serializer = LabelSerializer(data=request.data) @@ -734,22 +747,26 @@ class LabelViewSet(BaseViewSet): }, status=status.HTTP_400_BAD_REQUEST, ) + @invalidate_path_cache("/api/workspaces/:slug/labels/") - @invalidate_path_cache("/api/workspaces/:slug/projects/:project_id/issue-labels/") + @invalidate_path_cache( + "/api/workspaces/:slug/projects/:project_id/issue-labels/" + ) def partial_update(self, request, *args, **kwargs): return super().partial_update(request, *args, **kwargs) @invalidate_path_cache("/api/workspaces/:slug/labels/") - @invalidate_path_cache("/api/workspaces/:slug/projects/:project_id/issue-labels/") + @invalidate_path_cache( + "/api/workspaces/:slug/projects/:project_id/issue-labels/" + ) def destroy(self, request, *args, **kwargs): return super().destroy(request, *args, **kwargs) - + @cache_path_response(60 * 60 * 2) def list(self, request, *args, **kwargs): return super().list(request, *args, **kwargs) - class BulkDeleteIssuesEndpoint(BaseAPIView): permission_classes = [ ProjectEntityPermission, @@ -1964,5 +1981,82 @@ class IssueListEndpoint(BaseAPIView): .values("count") ) ).distinct() - serializer = IssueSerializer(queryset) + + 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)