From 96f00882badca5827db1696ff8ae62f79aedd23c Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Mon, 28 Aug 2023 19:12:46 +0530 Subject: [PATCH 1/3] chore: project public board issue retrieve --- apiserver/plane/api/serializers/__init__.py | 1 + apiserver/plane/api/serializers/issue.py | 32 ++++++++++++++++++--- apiserver/plane/api/urls.py | 6 ++++ apiserver/plane/api/views/__init__.py | 1 + apiserver/plane/api/views/issue.py | 32 +++++++++++++++++++-- 5 files changed, 66 insertions(+), 6 deletions(-) diff --git a/apiserver/plane/api/serializers/__init__.py b/apiserver/plane/api/serializers/__init__.py index 5855f0413b6..93b21a7f219 100644 --- a/apiserver/plane/api/serializers/__init__.py +++ b/apiserver/plane/api/serializers/__init__.py @@ -44,6 +44,7 @@ IssueReactionSerializer, CommentReactionSerializer, IssueVoteSerializer, + IssuePublicSerializer, ) from .module import ( diff --git a/apiserver/plane/api/serializers/issue.py b/apiserver/plane/api/serializers/issue.py index 64ee2b8f75d..3333d5f08f3 100644 --- a/apiserver/plane/api/serializers/issue.py +++ b/apiserver/plane/api/serializers/issue.py @@ -113,7 +113,11 @@ class Meta: ] def validate(self, data): - if data.get("start_date", None) is not None and data.get("target_date", None) is not None and data.get("start_date", None) > data.get("target_date", None): + if ( + data.get("start_date", None) is not None + and data.get("target_date", None) is not None + and data.get("start_date", None) > data.get("target_date", None) + ): raise serializers.ValidationError("Start date cannot exceed target date") return data @@ -554,9 +558,7 @@ class Meta: read_only_fields = ["workspace", "project", "comment", "actor"] - class IssueVoteSerializer(BaseSerializer): - class Meta: model = IssueVote fields = ["issue", "vote", "workspace_id", "project_id", "actor"] @@ -570,7 +572,6 @@ class IssueCommentSerializer(BaseSerializer): workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace") comment_reactions = CommentReactionLiteSerializer(read_only=True, many=True) - class Meta: model = IssueComment fields = "__all__" @@ -676,6 +677,29 @@ class Meta: ] +class IssuePublicSerializer(BaseSerializer): + project_detail = ProjectLiteSerializer(read_only=True, source="project") + state_detail = StateLiteSerializer(read_only=True, source="state") + issue_reactions = IssueReactionLiteSerializer(read_only=True, many=True) + + class Meta: + model = Issue + fields = [ + "id", + "name", + "description_html", + "sequence_id", + "state", + "state_detail", + "project", + "project_detail", + "workspace", + "priority", + "target_date", + ] + read_only_fields = fields + + class IssueSubscriberSerializer(BaseSerializer): class Meta: model = IssueSubscriber diff --git a/apiserver/plane/api/urls.py b/apiserver/plane/api/urls.py index a6beac6931d..b5848e25d5f 100644 --- a/apiserver/plane/api/urls.py +++ b/apiserver/plane/api/urls.py @@ -174,6 +174,7 @@ InboxIssuePublicViewSet, IssueVotePublicViewSet, WorkspaceProjectDeployBoardEndpoint, + IssueRetrievePublicViewSet, ## End Public Boards ## Exporter ExportIssuesEndpoint, @@ -1623,5 +1624,10 @@ WorkspaceProjectDeployBoardEndpoint.as_view(), name="workspace-project-boards", ), + path( + "public/workspaces//project-boards//issues//", + WorkspaceProjectDeployBoardEndpoint.as_view(), + name="workspace-project-boards", + ), ## End Public Boards ] diff --git a/apiserver/plane/api/views/__init__.py b/apiserver/plane/api/views/__init__.py index 9572c552f8a..c4e36fd9b27 100644 --- a/apiserver/plane/api/views/__init__.py +++ b/apiserver/plane/api/views/__init__.py @@ -85,6 +85,7 @@ IssueReactionPublicViewSet, CommentReactionPublicViewSet, IssueVotePublicViewSet, + IssueRetrievePublicViewSet, ) from .auth_extended import ( diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index 0b08bb14fd8..9dcd71758e2 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -28,6 +28,7 @@ from rest_framework.response import Response from rest_framework import status from rest_framework.parsers import MultiPartParser, FormParser +from rest_framework.permissions import AllowAny from sentry_sdk import capture_exception # Module imports @@ -49,6 +50,7 @@ IssueReactionSerializer, CommentReactionSerializer, IssueVoteSerializer, + IssuePublicSerializer, ) from plane.api.permissions import ( WorkspaceEntityPermission, @@ -769,7 +771,9 @@ def get(self, request, slug, project_id, issue_id): .order_by("state_group") ) - result = {item["state_group"]: item["state_count"] for item in state_distribution} + result = { + item["state_group"]: item["state_count"] for item in state_distribution + } serializer = IssueLiteSerializer( sub_issues, @@ -1567,7 +1571,8 @@ def partial_update(self, request, slug, project_id, issue_id, pk): except (IssueComment.DoesNotExist, ProjectDeployBoard.DoesNotExist): return Response( {"error": "IssueComent Does not exists"}, - status=status.HTTP_400_BAD_REQUEST,) + status=status.HTTP_400_BAD_REQUEST, + ) def destroy(self, request, slug, project_id, issue_id, pk): try: @@ -1827,3 +1832,26 @@ def destroy(self, request, slug, project_id, issue_id): status=status.HTTP_400_BAD_REQUEST, ) + +class IssueRetrievePublicViewSet(BaseAPIView): + permission_classes = [ + AllowAny, + ] + + def get(self, slug, project_id, issue_id): + try: + issue = Issue.objects.get( + workspace__slug=slug, project_id=project_id, issue_id=issue_id + ) + serializer = IssuePublicSerializer(issue) + return Response(serializer.data, status=status.HTTP_200_OK) + except Issue.DoesNotExist: + return Response( + {"error": "Issue Does not exist"}, status=status.HTTP_400_BAD_REQUEST + ) + except Exception as e: + capture_exception(e) + return Response( + {"error": "Something went wrong please try again later"}, + status=status.HTTP_400_BAD_REQUEST, + ) From d9316f2dd9e2738f38026c637377c8afa257837e Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Mon, 28 Aug 2023 19:21:26 +0530 Subject: [PATCH 2/3] dev: project issues list endpoint --- apiserver/plane/api/urls.py | 4 +- apiserver/plane/api/views/__init__.py | 2 +- apiserver/plane/api/views/issue.py | 150 ++++++++++++++++++++++++++ apiserver/plane/api/views/project.py | 148 ------------------------- 4 files changed, 153 insertions(+), 151 deletions(-) diff --git a/apiserver/plane/api/urls.py b/apiserver/plane/api/urls.py index b5848e25d5f..bb4282e51b4 100644 --- a/apiserver/plane/api/urls.py +++ b/apiserver/plane/api/urls.py @@ -167,7 +167,7 @@ ## End Notification # Public Boards ProjectDeployBoardViewSet, - ProjectDeployBoardIssuesPublicEndpoint, + ProjectIssuesPublicEndpoint, ProjectDeployBoardPublicSettingsEndpoint, IssueReactionPublicViewSet, CommentReactionPublicViewSet, @@ -1525,7 +1525,7 @@ ), path( "public/workspaces//project-boards//issues/", - ProjectDeployBoardIssuesPublicEndpoint.as_view(), + ProjectIssuesPublicEndpoint.as_view(), name="project-deploy-board", ), path( diff --git a/apiserver/plane/api/views/__init__.py b/apiserver/plane/api/views/__init__.py index c4e36fd9b27..58f3db9fefd 100644 --- a/apiserver/plane/api/views/__init__.py +++ b/apiserver/plane/api/views/__init__.py @@ -12,7 +12,6 @@ ProjectUserViewsEndpoint, ProjectMemberUserEndpoint, ProjectFavoritesViewSet, - ProjectDeployBoardIssuesPublicEndpoint, ProjectDeployBoardViewSet, ProjectDeployBoardPublicSettingsEndpoint, ProjectMemberEndpoint, @@ -86,6 +85,7 @@ CommentReactionPublicViewSet, IssueVotePublicViewSet, IssueRetrievePublicViewSet, + ProjectIssuesPublicEndpoint, ) from .auth_extended import ( diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index 9dcd71758e2..fbec6890c3b 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -1855,3 +1855,153 @@ def get(self, slug, project_id, issue_id): {"error": "Something went wrong please try again later"}, status=status.HTTP_400_BAD_REQUEST, ) + + +class ProjectIssuesPublicEndpoint(BaseAPIView): + permission_classes = [ + AllowAny, + ] + + def get(self, request, slug, project_id): + try: + project_deploy_board = ProjectDeployBoard.objects.get( + workspace__slug=slug, project_id=project_id + ) + + 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 = ( + Issue.issue_objects.annotate( + sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .filter(project_id=project_id) + .filter(workspace__slug=slug) + .select_related("project", "workspace", "state", "parent") + .prefetch_related("assignees", "labels") + .prefetch_related( + Prefetch( + "issue_reactions", + queryset=IssueReaction.objects.select_related("actor"), + ) + ) + .filter(**filters) + .annotate(cycle_id=F("issue_cycle__cycle_id")) + .annotate(module_id=F("issue_module__module_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") + ) + ) + + # 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) + + issues = IssuePublicSerializer(issue_queryset, many=True).data + + states = State.objects.filter( + workspace__slug=slug, project_id=project_id + ).values("name", "group", "color", "id") + + labels = Label.objects.filter( + workspace__slug=slug, project_id=project_id + ).values("id", "name", "color", "parent") + + ## Grouping the results + group_by = request.GET.get("group_by", False) + if group_by: + issues = group_results(issues, group_by) + + return Response( + { + "issues": issues, + "states": states, + "labels": labels, + }, + status=status.HTTP_200_OK, + ) + except ProjectDeployBoard.DoesNotExist: + return Response( + {"error": "Board does not exists"}, status=status.HTTP_404_NOT_FOUND + ) + except Exception as e: + capture_exception(e) + return Response( + {"error": "Something went wrong please try again later"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + diff --git a/apiserver/plane/api/views/project.py b/apiserver/plane/api/views/project.py index 3e5ca1c4b4a..97b06fce581 100644 --- a/apiserver/plane/api/views/project.py +++ b/apiserver/plane/api/views/project.py @@ -1143,154 +1143,6 @@ def get(self, request, slug, project_id): ) -class ProjectDeployBoardIssuesPublicEndpoint(BaseAPIView): - permission_classes = [ - AllowAny, - ] - - def get(self, request, slug, project_id): - try: - project_deploy_board = ProjectDeployBoard.objects.get( - workspace__slug=slug, project_id=project_id - ) - - 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 = ( - Issue.issue_objects.annotate( - sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id")) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .filter(project_id=project_id) - .filter(workspace__slug=slug) - .select_related("project", "workspace", "state", "parent") - .prefetch_related("assignees", "labels") - .prefetch_related( - Prefetch( - "issue_reactions", - queryset=IssueReaction.objects.select_related("actor"), - ) - ) - .filter(**filters) - .annotate(cycle_id=F("issue_cycle__cycle_id")) - .annotate(module_id=F("issue_module__module_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") - ) - ) - - # 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) - - issues = IssueLiteSerializer(issue_queryset, many=True).data - - states = State.objects.filter( - workspace__slug=slug, project_id=project_id - ).values("name", "group", "color", "id") - - labels = Label.objects.filter( - workspace__slug=slug, project_id=project_id - ).values("id", "name", "color", "parent") - - ## Grouping the results - group_by = request.GET.get("group_by", False) - if group_by: - issues = group_results(issues, group_by) - - return Response( - { - "issues": issues, - "states": states, - "labels": labels, - }, - status=status.HTTP_200_OK, - ) - except ProjectDeployBoard.DoesNotExist: - return Response( - {"error": "Board does not exists"}, status=status.HTTP_404_NOT_FOUND - ) - except Exception as e: - capture_exception(e) - return Response( - {"error": "Something went wrong please try again later"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - class WorkspaceProjectDeployBoardEndpoint(BaseAPIView): permission_classes = [AllowAny,] From 3bc2e5a191a2380251dccf1c9a85f82d5bd41840 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Mon, 28 Aug 2023 19:38:42 +0530 Subject: [PATCH 3/3] fix: issue public retrieve endpoint --- apiserver/plane/api/serializers/issue.py | 1 + apiserver/plane/api/urls.py | 12 ++++++------ apiserver/plane/api/views/__init__.py | 2 +- apiserver/plane/api/views/issue.py | 8 ++++---- 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/apiserver/plane/api/serializers/issue.py b/apiserver/plane/api/serializers/issue.py index 3333d5f08f3..43b53368455 100644 --- a/apiserver/plane/api/serializers/issue.py +++ b/apiserver/plane/api/serializers/issue.py @@ -696,6 +696,7 @@ class Meta: "workspace", "priority", "target_date", + "issue_reactions", ] read_only_fields = fields diff --git a/apiserver/plane/api/urls.py b/apiserver/plane/api/urls.py index bb4282e51b4..d1f3aa8a001 100644 --- a/apiserver/plane/api/urls.py +++ b/apiserver/plane/api/urls.py @@ -174,7 +174,7 @@ InboxIssuePublicViewSet, IssueVotePublicViewSet, WorkspaceProjectDeployBoardEndpoint, - IssueRetrievePublicViewSet, + IssueRetrievePublicEndpoint, ## End Public Boards ## Exporter ExportIssuesEndpoint, @@ -1528,6 +1528,11 @@ ProjectIssuesPublicEndpoint.as_view(), name="project-deploy-board", ), + path( + "public/workspaces//project-boards//issues//", + IssueRetrievePublicEndpoint.as_view(), + name="workspace-project-boards", + ), path( "public/workspaces//project-boards//issues//comments/", IssueCommentPublicViewSet.as_view( @@ -1624,10 +1629,5 @@ WorkspaceProjectDeployBoardEndpoint.as_view(), name="workspace-project-boards", ), - path( - "public/workspaces//project-boards//issues//", - WorkspaceProjectDeployBoardEndpoint.as_view(), - name="workspace-project-boards", - ), ## End Public Boards ] diff --git a/apiserver/plane/api/views/__init__.py b/apiserver/plane/api/views/__init__.py index 58f3db9fefd..2d0b36e8c5c 100644 --- a/apiserver/plane/api/views/__init__.py +++ b/apiserver/plane/api/views/__init__.py @@ -84,7 +84,7 @@ IssueReactionPublicViewSet, CommentReactionPublicViewSet, IssueVotePublicViewSet, - IssueRetrievePublicViewSet, + IssueRetrievePublicEndpoint, ProjectIssuesPublicEndpoint, ) diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index fbec6890c3b..56a0611e8bf 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -1833,15 +1833,15 @@ def destroy(self, request, slug, project_id, issue_id): ) -class IssueRetrievePublicViewSet(BaseAPIView): +class IssueRetrievePublicEndpoint(BaseAPIView): permission_classes = [ AllowAny, ] - def get(self, slug, project_id, issue_id): + def get(self, request, slug, project_id, issue_id): try: issue = Issue.objects.get( - workspace__slug=slug, project_id=project_id, issue_id=issue_id + workspace__slug=slug, project_id=project_id, pk=issue_id ) serializer = IssuePublicSerializer(issue) return Response(serializer.data, status=status.HTTP_200_OK) @@ -1850,7 +1850,7 @@ def get(self, slug, project_id, issue_id): {"error": "Issue Does not exist"}, status=status.HTTP_400_BAD_REQUEST ) except Exception as e: - capture_exception(e) + print(e) return Response( {"error": "Something went wrong please try again later"}, status=status.HTTP_400_BAD_REQUEST,