diff --git a/apiserver/plane/api/urls/issue.py b/apiserver/plane/api/urls/issue.py index 070ea8bd9..5ce9db85c 100644 --- a/apiserver/plane/api/urls/issue.py +++ b/apiserver/plane/api/urls/issue.py @@ -6,9 +6,15 @@ from plane.api.views import ( IssueLinkAPIEndpoint, IssueCommentAPIEndpoint, IssueActivityAPIEndpoint, + WorkspaceIssueAPIEndpoint, ) urlpatterns = [ + path( + "workspaces//issues/-/", + WorkspaceIssueAPIEndpoint.as_view(), + name="issue-by-identifier", + ), path( "workspaces//projects//issues/", IssueAPIEndpoint.as_view(), diff --git a/apiserver/plane/api/views/__init__.py b/apiserver/plane/api/views/__init__.py index 574ec69b6..d59b40fc5 100644 --- a/apiserver/plane/api/views/__init__.py +++ b/apiserver/plane/api/views/__init__.py @@ -3,6 +3,7 @@ from .project import ProjectAPIEndpoint, ProjectArchiveUnarchiveAPIEndpoint from .state import StateAPIEndpoint from .issue import ( + WorkspaceIssueAPIEndpoint, IssueAPIEndpoint, LabelAPIEndpoint, IssueLinkAPIEndpoint, diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index 53998c49f..46a6b6937 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -32,6 +32,7 @@ from plane.api.serializers import ( LabelSerializer, ) from plane.app.permissions import ( + WorkspaceEntityPermission, ProjectEntityPermission, ProjectLitePermission, ProjectMemberPermission, @@ -51,6 +52,65 @@ from plane.db.models import ( from .base import BaseAPIView, WebhookMixin + +class WorkspaceIssueAPIEndpoint(WebhookMixin, BaseAPIView): + """ + This viewset provides `retrieveByIssueId` on workspace level + + """ + + model = Issue + webhook_event = "issue" + permission_classes = [ + ProjectEntityPermission + ] + serializer_class = IssueSerializer + + + @property + def project__identifier(self): + return self.kwargs.get("project__identifier", None) + + def get_queryset(self): + return ( + 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(workspace__slug=self.kwargs.get("slug")) + .filter(project__identifier=self.kwargs.get("project__identifier")) + .select_related("project") + .select_related("workspace") + .select_related("state") + .select_related("parent") + .prefetch_related("assignees") + .prefetch_related("labels") + .order_by(self.kwargs.get("order_by", "-created_at")) + ).distinct() + + def get(self, request, slug, project__identifier=None, issue__identifier=None): + if issue__identifier and project__identifier: + issue = 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") + ).get(workspace__slug=slug, project__identifier=project__identifier, sequence_id=issue__identifier) + return Response( + IssueSerializer( + issue, + fields=self.fields, + expand=self.expand, + ).data, + status=status.HTTP_200_OK, + ) + class IssueAPIEndpoint(WebhookMixin, BaseAPIView): """ This viewset automatically provides `list`, `create`, `retrieve`, @@ -282,7 +342,7 @@ class IssueAPIEndpoint(WebhookMixin, BaseAPIView): ) if serializer.is_valid(): if ( - str(request.data.get("external_id")) + request.data.get("external_id") and (issue.external_id != str(request.data.get("external_id"))) and Issue.objects.filter( project_id=project_id, diff --git a/apiserver/plane/app/permissions/project.py b/apiserver/plane/app/permissions/project.py index 2ba2a1b64..25e5aaeb0 100644 --- a/apiserver/plane/app/permissions/project.py +++ b/apiserver/plane/app/permissions/project.py @@ -79,6 +79,16 @@ class ProjectEntityPermission(BasePermission): if request.user.is_anonymous: return False + # Handle requests based on project__identifier + if hasattr(view, "project__identifier") and view.project__identifier: + if request.method in SAFE_METHODS: + return ProjectMember.objects.filter( + workspace__slug=view.workspace_slug, + member=request.user, + project__identifier=view.project__identifier, + is_active=True, + ).exists() + ## Safe Methods -> Handle the filtering logic in queryset if request.method in SAFE_METHODS: return ProjectMember.objects.filter(