From 51e17643a2d48c071023c7180ec28df09a645c55 Mon Sep 17 00:00:00 2001 From: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com> Date: Mon, 20 Nov 2023 11:59:20 +0530 Subject: [PATCH] fix: file structuring (#2797) * fix: file structure changes * fix: pages update * fix: license imports changed --- apiserver/plane/api/apps.py | 5 - apiserver/plane/api/urls/public_board.py | 151 ---- apiserver/plane/{api => app}/__init__.py | 0 apiserver/plane/app/apps.py | 5 + .../{api => app}/permissions/__init__.py | 0 .../plane/{api => app}/permissions/project.py | 0 .../{api => app}/permissions/workspace.py | 0 .../{api => app}/serializers/__init__.py | 0 .../{api => app}/serializers/analytic.py | 0 .../plane/{api => app}/serializers/api.py | 0 .../plane/{api => app}/serializers/asset.py | 0 .../plane/{api => app}/serializers/base.py | 0 .../plane/{api => app}/serializers/cycle.py | 0 .../{api => app}/serializers/estimate.py | 2 +- .../{api => app}/serializers/exporter.py | 0 .../{api => app}/serializers/importer.py | 0 .../plane/{api => app}/serializers/inbox.py | 0 .../serializers/integration/__init__.py | 0 .../serializers/integration/base.py | 2 +- .../serializers/integration/github.py | 2 +- .../serializers/integration/slack.py | 2 +- .../plane/{api => app}/serializers/issue.py | 0 .../plane/{api => app}/serializers/module.py | 0 .../{api => app}/serializers/notification.py | 0 .../plane/{api => app}/serializers/page.py | 0 .../plane/{api => app}/serializers/project.py | 4 +- .../plane/{api => app}/serializers/state.py | 3 +- .../plane/{api => app}/serializers/user.py | 0 .../plane/{api => app}/serializers/view.py | 0 .../plane/{api => app}/serializers/webhook.py | 0 .../{api => app}/serializers/workspace.py | 0 apiserver/plane/{api => app}/urls/__init__.py | 2 - apiserver/plane/{api => app}/urls/analytic.py | 2 +- apiserver/plane/{api => app}/urls/api.py | 2 +- apiserver/plane/{api => app}/urls/asset.py | 2 +- .../plane/{api => app}/urls/authentication.py | 2 +- apiserver/plane/{api => app}/urls/config.py | 2 +- apiserver/plane/{api => app}/urls/cycle.py | 2 +- apiserver/plane/{api => app}/urls/estimate.py | 2 +- apiserver/plane/{api => app}/urls/external.py | 6 +- apiserver/plane/{api => app}/urls/importer.py | 2 +- apiserver/plane/{api => app}/urls/inbox.py | 2 +- .../plane/{api => app}/urls/integration.py | 2 +- apiserver/plane/{api => app}/urls/issue.py | 2 +- apiserver/plane/{api => app}/urls/module.py | 2 +- .../plane/{api => app}/urls/notification.py | 2 +- apiserver/plane/{api => app}/urls/page.py | 2 +- apiserver/plane/{api => app}/urls/project.py | 26 +- apiserver/plane/{api => app}/urls/search.py | 2 +- apiserver/plane/{api => app}/urls/state.py | 2 +- apiserver/plane/{api => app}/urls/user.py | 2 +- apiserver/plane/{api => app}/urls/views.py | 2 +- apiserver/plane/{api => app}/urls/webhook.py | 2 +- .../plane/{api => app}/urls/workspace.py | 2 +- .../plane/{api => app}/urls_deprecated.py | 2 +- .../plane/{api => app}/views/__init__.py | 12 +- .../plane/{api => app}/views/analytic.py | 7 +- apiserver/plane/{api => app}/views/api.py | 4 +- apiserver/plane/{api => app}/views/asset.py | 5 +- .../plane/{api => app}/views/auth_extended.py | 2 +- .../{api => app}/views/authentication.py | 1 + apiserver/plane/{api => app}/views/base.py | 0 apiserver/plane/{api => app}/views/config.py | 1 - apiserver/plane/{api => app}/views/cycle.py | 5 +- .../plane/{api => app}/views/estimate.py | 5 +- .../plane/{api => app}/views/exporter.py | 5 +- .../plane/{api => app}/views/external.py | 6 +- .../plane/{api => app}/views/importer.py | 7 +- apiserver/plane/{api => app}/views/inbox.py | 256 +------ .../views/integration/__init__.py | 0 .../{api => app}/views/integration/base.py | 6 +- .../{api => app}/views/integration/github.py | 6 +- .../{api => app}/views/integration/slack.py | 6 +- apiserver/plane/{api => app}/views/issue.py | 608 +--------------- apiserver/plane/{api => app}/views/module.py | 6 +- .../plane/{api => app}/views/notification.py | 3 +- apiserver/plane/{api => app}/views/oauth.py | 1 - apiserver/plane/{api => app}/views/page.py | 74 +- apiserver/plane/{api => app}/views/project.py | 113 +-- apiserver/plane/{api => app}/views/search.py | 1 - apiserver/plane/{api => app}/views/state.py | 7 +- apiserver/plane/{api => app}/views/user.py | 5 +- apiserver/plane/{api => app}/views/view.py | 7 +- apiserver/plane/{api => app}/views/webhook.py | 4 +- .../plane/{api => app}/views/workspace.py | 8 +- apiserver/plane/bgtasks/importer_task.py | 2 +- .../plane/bgtasks/issue_activites_task.py | 5 +- .../plane/license/api/serializers/instance.py | 4 +- apiserver/plane/license/api/views/instance.py | 2 +- apiserver/plane/settings/common.py | 3 +- apiserver/plane/space/__init__.py | 0 apiserver/plane/space/apps.py | 5 + apiserver/plane/space/serializer/__init__.py | 5 + apiserver/plane/space/serializer/base.py | 58 ++ apiserver/plane/space/serializer/cycle.py | 18 + apiserver/plane/space/serializer/inbox.py | 47 ++ apiserver/plane/space/serializer/issue.py | 506 ++++++++++++++ apiserver/plane/space/serializer/module.py | 18 + apiserver/plane/space/serializer/project.py | 20 + apiserver/plane/space/serializer/state.py | 28 + apiserver/plane/space/serializer/user.py | 22 + apiserver/plane/space/serializer/workspace.py | 15 + apiserver/plane/space/urls/__init__.py | 10 + apiserver/plane/space/urls/inbox.py | 49 ++ apiserver/plane/space/urls/issue.py | 76 ++ apiserver/plane/space/urls/project.py | 20 + apiserver/plane/space/views/__init__.py | 15 + apiserver/plane/space/views/base.py | 212 ++++++ apiserver/plane/space/views/inbox.py | 282 ++++++++ apiserver/plane/space/views/issue.py | 656 ++++++++++++++++++ apiserver/plane/space/views/project.py | 61 ++ apiserver/plane/tests/api/base.py | 2 +- apiserver/plane/urls.py | 3 +- 113 files changed, 2306 insertions(+), 1261 deletions(-) delete mode 100644 apiserver/plane/api/apps.py delete mode 100644 apiserver/plane/api/urls/public_board.py rename apiserver/plane/{api => app}/__init__.py (100%) create mode 100644 apiserver/plane/app/apps.py rename apiserver/plane/{api => app}/permissions/__init__.py (100%) rename apiserver/plane/{api => app}/permissions/project.py (100%) rename apiserver/plane/{api => app}/permissions/workspace.py (100%) rename apiserver/plane/{api => app}/serializers/__init__.py (100%) rename apiserver/plane/{api => app}/serializers/analytic.py (100%) rename apiserver/plane/{api => app}/serializers/api.py (100%) rename apiserver/plane/{api => app}/serializers/asset.py (100%) rename apiserver/plane/{api => app}/serializers/base.py (100%) rename apiserver/plane/{api => app}/serializers/cycle.py (100%) rename apiserver/plane/{api => app}/serializers/estimate.py (94%) rename apiserver/plane/{api => app}/serializers/exporter.py (100%) rename apiserver/plane/{api => app}/serializers/importer.py (100%) rename apiserver/plane/{api => app}/serializers/inbox.py (100%) rename apiserver/plane/{api => app}/serializers/integration/__init__.py (100%) rename apiserver/plane/{api => app}/serializers/integration/base.py (90%) rename apiserver/plane/{api => app}/serializers/integration/github.py (95%) rename apiserver/plane/{api => app}/serializers/integration/slack.py (86%) rename apiserver/plane/{api => app}/serializers/issue.py (100%) rename apiserver/plane/{api => app}/serializers/module.py (100%) rename apiserver/plane/{api => app}/serializers/notification.py (100%) rename apiserver/plane/{api => app}/serializers/page.py (100%) rename apiserver/plane/{api => app}/serializers/project.py (97%) rename apiserver/plane/{api => app}/serializers/state.py (84%) rename apiserver/plane/{api => app}/serializers/user.py (100%) rename apiserver/plane/{api => app}/serializers/view.py (100%) rename apiserver/plane/{api => app}/serializers/webhook.py (100%) rename apiserver/plane/{api => app}/serializers/workspace.py (100%) rename apiserver/plane/{api => app}/urls/__init__.py (94%) rename apiserver/plane/{api => app}/urls/analytic.py (97%) rename apiserver/plane/{api => app}/urls/api.py (88%) rename apiserver/plane/{api => app}/urls/asset.py (95%) rename apiserver/plane/{api => app}/urls/authentication.py (98%) rename apiserver/plane/{api => app}/urls/config.py (75%) rename apiserver/plane/{api => app}/urls/cycle.py (98%) rename apiserver/plane/{api => app}/urls/estimate.py (96%) rename apiserver/plane/{api => app}/urls/external.py (74%) rename apiserver/plane/{api => app}/urls/importer.py (96%) rename apiserver/plane/{api => app}/urls/inbox.py (97%) rename apiserver/plane/{api => app}/urls/integration.py (99%) rename apiserver/plane/{api => app}/urls/issue.py (99%) rename apiserver/plane/{api => app}/urls/module.py (99%) rename apiserver/plane/{api => app}/urls/notification.py (98%) rename apiserver/plane/{api => app}/urls/page.py (99%) rename apiserver/plane/{api => app}/urls/project.py (85%) rename apiserver/plane/{api => app}/urls/search.py (93%) rename apiserver/plane/{api => app}/urls/state.py (95%) rename apiserver/plane/{api => app}/urls/user.py (98%) rename apiserver/plane/{api => app}/urls/views.py (98%) rename apiserver/plane/{api => app}/urls/webhook.py (95%) rename apiserver/plane/{api => app}/urls/workspace.py (99%) rename apiserver/plane/{api => app}/urls_deprecated.py (99%) rename apiserver/plane/{api => app}/views/__init__.py (92%) rename apiserver/plane/{api => app}/views/analytic.py (98%) rename apiserver/plane/{api => app}/views/api.py (95%) rename apiserver/plane/{api => app}/views/asset.py (95%) rename apiserver/plane/{api => app}/views/auth_extended.py (99%) rename apiserver/plane/{api => app}/views/authentication.py (99%) rename apiserver/plane/{api => app}/views/base.py (100%) rename apiserver/plane/{api => app}/views/config.py (98%) rename apiserver/plane/{api => app}/views/cycle.py (99%) rename apiserver/plane/{api => app}/views/estimate.py (97%) rename apiserver/plane/{api => app}/views/exporter.py (94%) rename apiserver/plane/{api => app}/views/external.py (94%) rename apiserver/plane/{api => app}/views/importer.py (99%) rename apiserver/plane/{api => app}/views/inbox.py (56%) rename apiserver/plane/{api => app}/views/integration/__init__.py (100%) rename apiserver/plane/{api => app}/views/integration/base.py (97%) rename apiserver/plane/{api => app}/views/integration/github.py (97%) rename apiserver/plane/{api => app}/views/integration/slack.py (94%) rename apiserver/plane/{api => app}/views/issue.py (73%) rename apiserver/plane/{api => app}/views/module.py (99%) rename apiserver/plane/{api => app}/views/notification.py (99%) rename apiserver/plane/{api => app}/views/oauth.py (99%) rename apiserver/plane/{api => app}/views/page.py (90%) rename apiserver/plane/{api => app}/views/project.py (96%) rename apiserver/plane/{api => app}/views/search.py (99%) rename apiserver/plane/{api => app}/views/state.py (94%) rename apiserver/plane/{api => app}/views/user.py (95%) rename apiserver/plane/{api => app}/views/view.py (97%) rename apiserver/plane/{api => app}/views/webhook.py (97%) rename apiserver/plane/{api => app}/views/workspace.py (99%) create mode 100644 apiserver/plane/space/__init__.py create mode 100644 apiserver/plane/space/apps.py create mode 100644 apiserver/plane/space/serializer/__init__.py create mode 100644 apiserver/plane/space/serializer/base.py create mode 100644 apiserver/plane/space/serializer/cycle.py create mode 100644 apiserver/plane/space/serializer/inbox.py create mode 100644 apiserver/plane/space/serializer/issue.py create mode 100644 apiserver/plane/space/serializer/module.py create mode 100644 apiserver/plane/space/serializer/project.py create mode 100644 apiserver/plane/space/serializer/state.py create mode 100644 apiserver/plane/space/serializer/user.py create mode 100644 apiserver/plane/space/serializer/workspace.py create mode 100644 apiserver/plane/space/urls/__init__.py create mode 100644 apiserver/plane/space/urls/inbox.py create mode 100644 apiserver/plane/space/urls/issue.py create mode 100644 apiserver/plane/space/urls/project.py create mode 100644 apiserver/plane/space/views/__init__.py create mode 100644 apiserver/plane/space/views/base.py create mode 100644 apiserver/plane/space/views/inbox.py create mode 100644 apiserver/plane/space/views/issue.py create mode 100644 apiserver/plane/space/views/project.py diff --git a/apiserver/plane/api/apps.py b/apiserver/plane/api/apps.py deleted file mode 100644 index 6ba36e7e5..000000000 --- a/apiserver/plane/api/apps.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.apps import AppConfig - - -class ApiConfig(AppConfig): - name = "plane.api" diff --git a/apiserver/plane/api/urls/public_board.py b/apiserver/plane/api/urls/public_board.py deleted file mode 100644 index 272d5961c..000000000 --- a/apiserver/plane/api/urls/public_board.py +++ /dev/null @@ -1,151 +0,0 @@ -from django.urls import path - - -from plane.api.views import ( - ProjectDeployBoardViewSet, - ProjectDeployBoardPublicSettingsEndpoint, - ProjectIssuesPublicEndpoint, - IssueRetrievePublicEndpoint, - IssueCommentPublicViewSet, - IssueReactionPublicViewSet, - CommentReactionPublicViewSet, - InboxIssuePublicViewSet, - IssueVotePublicViewSet, - WorkspaceProjectDeployBoardEndpoint, -) - - -urlpatterns = [ - path( - "workspaces//projects//project-deploy-boards/", - ProjectDeployBoardViewSet.as_view( - { - "get": "list", - "post": "create", - } - ), - name="project-deploy-board", - ), - path( - "workspaces//projects//project-deploy-boards//", - ProjectDeployBoardViewSet.as_view( - { - "get": "retrieve", - "patch": "partial_update", - "delete": "destroy", - } - ), - name="project-deploy-board", - ), - path( - "public/workspaces//project-boards//settings/", - ProjectDeployBoardPublicSettingsEndpoint.as_view(), - name="project-deploy-board-settings", - ), - path( - "public/workspaces//project-boards//issues/", - 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( - { - "get": "list", - "post": "create", - } - ), - name="issue-comments-project-board", - ), - path( - "public/workspaces//project-boards//issues//comments//", - IssueCommentPublicViewSet.as_view( - { - "get": "retrieve", - "patch": "partial_update", - "delete": "destroy", - } - ), - name="issue-comments-project-board", - ), - path( - "public/workspaces//project-boards//issues//reactions/", - IssueReactionPublicViewSet.as_view( - { - "get": "list", - "post": "create", - } - ), - name="issue-reactions-project-board", - ), - path( - "public/workspaces//project-boards//issues//reactions//", - IssueReactionPublicViewSet.as_view( - { - "delete": "destroy", - } - ), - name="issue-reactions-project-board", - ), - path( - "public/workspaces//project-boards//comments//reactions/", - CommentReactionPublicViewSet.as_view( - { - "get": "list", - "post": "create", - } - ), - name="comment-reactions-project-board", - ), - path( - "public/workspaces//project-boards//comments//reactions//", - CommentReactionPublicViewSet.as_view( - { - "delete": "destroy", - } - ), - name="comment-reactions-project-board", - ), - path( - "public/workspaces//project-boards//inboxes//inbox-issues/", - InboxIssuePublicViewSet.as_view( - { - "get": "list", - "post": "create", - } - ), - name="inbox-issue", - ), - path( - "public/workspaces//project-boards//inboxes//inbox-issues//", - InboxIssuePublicViewSet.as_view( - { - "get": "retrieve", - "patch": "partial_update", - "delete": "destroy", - } - ), - name="inbox-issue", - ), - path( - "public/workspaces//project-boards//issues//votes/", - IssueVotePublicViewSet.as_view( - { - "get": "list", - "post": "create", - "delete": "destroy", - } - ), - name="issue-vote-project-board", - ), - path( - "public/workspaces//project-boards/", - WorkspaceProjectDeployBoardEndpoint.as_view(), - name="workspace-project-boards", - ), -] diff --git a/apiserver/plane/api/__init__.py b/apiserver/plane/app/__init__.py similarity index 100% rename from apiserver/plane/api/__init__.py rename to apiserver/plane/app/__init__.py diff --git a/apiserver/plane/app/apps.py b/apiserver/plane/app/apps.py new file mode 100644 index 000000000..6057d131a --- /dev/null +++ b/apiserver/plane/app/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class AppConfig(AppConfig): + name = "plane.app" diff --git a/apiserver/plane/api/permissions/__init__.py b/apiserver/plane/app/permissions/__init__.py similarity index 100% rename from apiserver/plane/api/permissions/__init__.py rename to apiserver/plane/app/permissions/__init__.py diff --git a/apiserver/plane/api/permissions/project.py b/apiserver/plane/app/permissions/project.py similarity index 100% rename from apiserver/plane/api/permissions/project.py rename to apiserver/plane/app/permissions/project.py diff --git a/apiserver/plane/api/permissions/workspace.py b/apiserver/plane/app/permissions/workspace.py similarity index 100% rename from apiserver/plane/api/permissions/workspace.py rename to apiserver/plane/app/permissions/workspace.py diff --git a/apiserver/plane/api/serializers/__init__.py b/apiserver/plane/app/serializers/__init__.py similarity index 100% rename from apiserver/plane/api/serializers/__init__.py rename to apiserver/plane/app/serializers/__init__.py diff --git a/apiserver/plane/api/serializers/analytic.py b/apiserver/plane/app/serializers/analytic.py similarity index 100% rename from apiserver/plane/api/serializers/analytic.py rename to apiserver/plane/app/serializers/analytic.py diff --git a/apiserver/plane/api/serializers/api.py b/apiserver/plane/app/serializers/api.py similarity index 100% rename from apiserver/plane/api/serializers/api.py rename to apiserver/plane/app/serializers/api.py diff --git a/apiserver/plane/api/serializers/asset.py b/apiserver/plane/app/serializers/asset.py similarity index 100% rename from apiserver/plane/api/serializers/asset.py rename to apiserver/plane/app/serializers/asset.py diff --git a/apiserver/plane/api/serializers/base.py b/apiserver/plane/app/serializers/base.py similarity index 100% rename from apiserver/plane/api/serializers/base.py rename to apiserver/plane/app/serializers/base.py diff --git a/apiserver/plane/api/serializers/cycle.py b/apiserver/plane/app/serializers/cycle.py similarity index 100% rename from apiserver/plane/api/serializers/cycle.py rename to apiserver/plane/app/serializers/cycle.py diff --git a/apiserver/plane/api/serializers/estimate.py b/apiserver/plane/app/serializers/estimate.py similarity index 94% rename from apiserver/plane/api/serializers/estimate.py rename to apiserver/plane/app/serializers/estimate.py index 3cb0e4713..4a1cda779 100644 --- a/apiserver/plane/api/serializers/estimate.py +++ b/apiserver/plane/app/serializers/estimate.py @@ -2,7 +2,7 @@ from .base import BaseSerializer from plane.db.models import Estimate, EstimatePoint -from plane.api.serializers import WorkspaceLiteSerializer, ProjectLiteSerializer +from plane.app.serializers import WorkspaceLiteSerializer, ProjectLiteSerializer class EstimateSerializer(BaseSerializer): diff --git a/apiserver/plane/api/serializers/exporter.py b/apiserver/plane/app/serializers/exporter.py similarity index 100% rename from apiserver/plane/api/serializers/exporter.py rename to apiserver/plane/app/serializers/exporter.py diff --git a/apiserver/plane/api/serializers/importer.py b/apiserver/plane/app/serializers/importer.py similarity index 100% rename from apiserver/plane/api/serializers/importer.py rename to apiserver/plane/app/serializers/importer.py diff --git a/apiserver/plane/api/serializers/inbox.py b/apiserver/plane/app/serializers/inbox.py similarity index 100% rename from apiserver/plane/api/serializers/inbox.py rename to apiserver/plane/app/serializers/inbox.py diff --git a/apiserver/plane/api/serializers/integration/__init__.py b/apiserver/plane/app/serializers/integration/__init__.py similarity index 100% rename from apiserver/plane/api/serializers/integration/__init__.py rename to apiserver/plane/app/serializers/integration/__init__.py diff --git a/apiserver/plane/api/serializers/integration/base.py b/apiserver/plane/app/serializers/integration/base.py similarity index 90% rename from apiserver/plane/api/serializers/integration/base.py rename to apiserver/plane/app/serializers/integration/base.py index 10ebd4620..6f6543b9e 100644 --- a/apiserver/plane/api/serializers/integration/base.py +++ b/apiserver/plane/app/serializers/integration/base.py @@ -1,5 +1,5 @@ # Module imports -from plane.api.serializers import BaseSerializer +from plane.app.serializers import BaseSerializer from plane.db.models import Integration, WorkspaceIntegration diff --git a/apiserver/plane/api/serializers/integration/github.py b/apiserver/plane/app/serializers/integration/github.py similarity index 95% rename from apiserver/plane/api/serializers/integration/github.py rename to apiserver/plane/app/serializers/integration/github.py index 8352dcee1..850bccf1b 100644 --- a/apiserver/plane/api/serializers/integration/github.py +++ b/apiserver/plane/app/serializers/integration/github.py @@ -1,5 +1,5 @@ # Module imports -from plane.api.serializers import BaseSerializer +from plane.app.serializers import BaseSerializer from plane.db.models import ( GithubIssueSync, GithubRepository, diff --git a/apiserver/plane/api/serializers/integration/slack.py b/apiserver/plane/app/serializers/integration/slack.py similarity index 86% rename from apiserver/plane/api/serializers/integration/slack.py rename to apiserver/plane/app/serializers/integration/slack.py index f535a64de..9c461c5b9 100644 --- a/apiserver/plane/api/serializers/integration/slack.py +++ b/apiserver/plane/app/serializers/integration/slack.py @@ -1,5 +1,5 @@ # Module imports -from plane.api.serializers import BaseSerializer +from plane.app.serializers import BaseSerializer from plane.db.models import SlackProjectSync diff --git a/apiserver/plane/api/serializers/issue.py b/apiserver/plane/app/serializers/issue.py similarity index 100% rename from apiserver/plane/api/serializers/issue.py rename to apiserver/plane/app/serializers/issue.py diff --git a/apiserver/plane/api/serializers/module.py b/apiserver/plane/app/serializers/module.py similarity index 100% rename from apiserver/plane/api/serializers/module.py rename to apiserver/plane/app/serializers/module.py diff --git a/apiserver/plane/api/serializers/notification.py b/apiserver/plane/app/serializers/notification.py similarity index 100% rename from apiserver/plane/api/serializers/notification.py rename to apiserver/plane/app/serializers/notification.py diff --git a/apiserver/plane/api/serializers/page.py b/apiserver/plane/app/serializers/page.py similarity index 100% rename from apiserver/plane/api/serializers/page.py rename to apiserver/plane/app/serializers/page.py diff --git a/apiserver/plane/api/serializers/project.py b/apiserver/plane/app/serializers/project.py similarity index 97% rename from apiserver/plane/api/serializers/project.py rename to apiserver/plane/app/serializers/project.py index 9ecae555c..e9bdf7be4 100644 --- a/apiserver/plane/api/serializers/project.py +++ b/apiserver/plane/app/serializers/project.py @@ -3,8 +3,8 @@ from rest_framework import serializers # Module imports from .base import BaseSerializer, DynamicBaseSerializer -from plane.api.serializers.workspace import WorkSpaceSerializer, WorkspaceLiteSerializer -from plane.api.serializers.user import UserLiteSerializer, UserAdminLiteSerializer +from plane.app.serializers.workspace import WorkspaceLiteSerializer +from plane.app.serializers.user import UserLiteSerializer, UserAdminLiteSerializer from plane.db.models import ( Project, ProjectMember, diff --git a/apiserver/plane/api/serializers/state.py b/apiserver/plane/app/serializers/state.py similarity index 84% rename from apiserver/plane/api/serializers/state.py rename to apiserver/plane/app/serializers/state.py index ad416c340..7cf645fae 100644 --- a/apiserver/plane/api/serializers/state.py +++ b/apiserver/plane/app/serializers/state.py @@ -1,7 +1,6 @@ # Module imports from .base import BaseSerializer -from .workspace import WorkspaceLiteSerializer -from .project import ProjectLiteSerializer + from plane.db.models import State diff --git a/apiserver/plane/api/serializers/user.py b/apiserver/plane/app/serializers/user.py similarity index 100% rename from apiserver/plane/api/serializers/user.py rename to apiserver/plane/app/serializers/user.py diff --git a/apiserver/plane/api/serializers/view.py b/apiserver/plane/app/serializers/view.py similarity index 100% rename from apiserver/plane/api/serializers/view.py rename to apiserver/plane/app/serializers/view.py diff --git a/apiserver/plane/api/serializers/webhook.py b/apiserver/plane/app/serializers/webhook.py similarity index 100% rename from apiserver/plane/api/serializers/webhook.py rename to apiserver/plane/app/serializers/webhook.py diff --git a/apiserver/plane/api/serializers/workspace.py b/apiserver/plane/app/serializers/workspace.py similarity index 100% rename from apiserver/plane/api/serializers/workspace.py rename to apiserver/plane/app/serializers/workspace.py diff --git a/apiserver/plane/api/urls/__init__.py b/apiserver/plane/app/urls/__init__.py similarity index 94% rename from apiserver/plane/api/urls/__init__.py rename to apiserver/plane/app/urls/__init__.py index e6088cb14..7d057ad9e 100644 --- a/apiserver/plane/api/urls/__init__.py +++ b/apiserver/plane/app/urls/__init__.py @@ -13,7 +13,6 @@ from .module import urlpatterns as module_urls from .notification import urlpatterns as notification_urls from .page import urlpatterns as page_urls from .project import urlpatterns as project_urls -from .public_board import urlpatterns as public_board_urls from .search import urlpatterns as search_urls from .state import urlpatterns as state_urls from .user import urlpatterns as user_urls @@ -43,7 +42,6 @@ urlpatterns = [ *notification_urls, *page_urls, *project_urls, - *public_board_urls, *search_urls, *state_urls, *user_urls, diff --git a/apiserver/plane/api/urls/analytic.py b/apiserver/plane/app/urls/analytic.py similarity index 97% rename from apiserver/plane/api/urls/analytic.py rename to apiserver/plane/app/urls/analytic.py index cb6155e32..668268350 100644 --- a/apiserver/plane/api/urls/analytic.py +++ b/apiserver/plane/app/urls/analytic.py @@ -1,7 +1,7 @@ from django.urls import path -from plane.api.views import ( +from plane.app.views import ( AnalyticsEndpoint, AnalyticViewViewset, SavedAnalyticEndpoint, diff --git a/apiserver/plane/api/urls/api.py b/apiserver/plane/app/urls/api.py similarity index 88% rename from apiserver/plane/api/urls/api.py rename to apiserver/plane/app/urls/api.py index 1a2862045..b77ea8530 100644 --- a/apiserver/plane/api/urls/api.py +++ b/apiserver/plane/app/urls/api.py @@ -1,5 +1,5 @@ from django.urls import path -from plane.api.views import ApiTokenEndpoint +from plane.app.views import ApiTokenEndpoint urlpatterns = [ # API Tokens diff --git a/apiserver/plane/api/urls/asset.py b/apiserver/plane/app/urls/asset.py similarity index 95% rename from apiserver/plane/api/urls/asset.py rename to apiserver/plane/app/urls/asset.py index b6ae9f42c..11ec8b8e8 100644 --- a/apiserver/plane/api/urls/asset.py +++ b/apiserver/plane/app/urls/asset.py @@ -1,7 +1,7 @@ from django.urls import path -from plane.api.views import ( +from plane.app.views import ( FileAssetEndpoint, UserAssetsEndpoint, ) diff --git a/apiserver/plane/api/urls/authentication.py b/apiserver/plane/app/urls/authentication.py similarity index 98% rename from apiserver/plane/api/urls/authentication.py rename to apiserver/plane/app/urls/authentication.py index 44b7000ea..6111075f2 100644 --- a/apiserver/plane/api/urls/authentication.py +++ b/apiserver/plane/app/urls/authentication.py @@ -3,7 +3,7 @@ from django.urls import path from rest_framework_simplejwt.views import TokenRefreshView -from plane.api.views import ( +from plane.app.views import ( # Authentication SignUpEndpoint, SignInEndpoint, diff --git a/apiserver/plane/api/urls/config.py b/apiserver/plane/app/urls/config.py similarity index 75% rename from apiserver/plane/api/urls/config.py rename to apiserver/plane/app/urls/config.py index 321a56200..12beb63aa 100644 --- a/apiserver/plane/api/urls/config.py +++ b/apiserver/plane/app/urls/config.py @@ -1,7 +1,7 @@ from django.urls import path -from plane.api.views import ConfigurationEndpoint +from plane.app.views import ConfigurationEndpoint urlpatterns = [ path( diff --git a/apiserver/plane/api/urls/cycle.py b/apiserver/plane/app/urls/cycle.py similarity index 98% rename from apiserver/plane/api/urls/cycle.py rename to apiserver/plane/app/urls/cycle.py index 7e6f014fc..0e786e291 100644 --- a/apiserver/plane/api/urls/cycle.py +++ b/apiserver/plane/app/urls/cycle.py @@ -1,7 +1,7 @@ from django.urls import path -from plane.api.views import ( +from plane.app.views import ( CycleViewSet, CycleIssueViewSet, CycleDateCheckEndpoint, diff --git a/apiserver/plane/api/urls/estimate.py b/apiserver/plane/app/urls/estimate.py similarity index 96% rename from apiserver/plane/api/urls/estimate.py rename to apiserver/plane/app/urls/estimate.py index 89363e849..d8571ff0c 100644 --- a/apiserver/plane/api/urls/estimate.py +++ b/apiserver/plane/app/urls/estimate.py @@ -1,7 +1,7 @@ from django.urls import path -from plane.api.views import ( +from plane.app.views import ( ProjectEstimatePointEndpoint, BulkEstimatePointEndpoint, ) diff --git a/apiserver/plane/api/urls/external.py b/apiserver/plane/app/urls/external.py similarity index 74% rename from apiserver/plane/api/urls/external.py rename to apiserver/plane/app/urls/external.py index c22289035..774e6fb7c 100644 --- a/apiserver/plane/api/urls/external.py +++ b/apiserver/plane/app/urls/external.py @@ -1,9 +1,9 @@ from django.urls import path -from plane.api.views import UnsplashEndpoint -from plane.api.views import ReleaseNotesEndpoint -from plane.api.views import GPTIntegrationEndpoint +from plane.app.views import UnsplashEndpoint +from plane.app.views import ReleaseNotesEndpoint +from plane.app.views import GPTIntegrationEndpoint urlpatterns = [ diff --git a/apiserver/plane/api/urls/importer.py b/apiserver/plane/app/urls/importer.py similarity index 96% rename from apiserver/plane/api/urls/importer.py rename to apiserver/plane/app/urls/importer.py index c0a9aa5b5..f3a018d78 100644 --- a/apiserver/plane/api/urls/importer.py +++ b/apiserver/plane/app/urls/importer.py @@ -1,7 +1,7 @@ from django.urls import path -from plane.api.views import ( +from plane.app.views import ( ServiceIssueImportSummaryEndpoint, ImportServiceEndpoint, UpdateServiceImportStatusEndpoint, diff --git a/apiserver/plane/api/urls/inbox.py b/apiserver/plane/app/urls/inbox.py similarity index 97% rename from apiserver/plane/api/urls/inbox.py rename to apiserver/plane/app/urls/inbox.py index 315f30601..16ea40b21 100644 --- a/apiserver/plane/api/urls/inbox.py +++ b/apiserver/plane/app/urls/inbox.py @@ -1,7 +1,7 @@ from django.urls import path -from plane.api.views import ( +from plane.app.views import ( InboxViewSet, InboxIssueViewSet, ) diff --git a/apiserver/plane/api/urls/integration.py b/apiserver/plane/app/urls/integration.py similarity index 99% rename from apiserver/plane/api/urls/integration.py rename to apiserver/plane/app/urls/integration.py index dd431b6c8..cf3f82d5a 100644 --- a/apiserver/plane/api/urls/integration.py +++ b/apiserver/plane/app/urls/integration.py @@ -1,7 +1,7 @@ from django.urls import path -from plane.api.views import ( +from plane.app.views import ( IntegrationViewSet, WorkspaceIntegrationViewSet, GithubRepositoriesEndpoint, diff --git a/apiserver/plane/api/urls/issue.py b/apiserver/plane/app/urls/issue.py similarity index 99% rename from apiserver/plane/api/urls/issue.py rename to apiserver/plane/app/urls/issue.py index 23a8e4fa6..9aa189288 100644 --- a/apiserver/plane/api/urls/issue.py +++ b/apiserver/plane/app/urls/issue.py @@ -1,7 +1,7 @@ from django.urls import path -from plane.api.views import ( +from plane.app.views import ( IssueViewSet, IssueListEndpoint, IssueListGroupedEndpoint, diff --git a/apiserver/plane/api/urls/module.py b/apiserver/plane/app/urls/module.py similarity index 99% rename from apiserver/plane/api/urls/module.py rename to apiserver/plane/app/urls/module.py index d9ca849ed..20c9d9e35 100644 --- a/apiserver/plane/api/urls/module.py +++ b/apiserver/plane/app/urls/module.py @@ -1,7 +1,7 @@ from django.urls import path -from plane.api.views import ( +from plane.app.views import ( ModuleViewSet, ModuleIssueViewSet, ModuleLinkViewSet, diff --git a/apiserver/plane/api/urls/notification.py b/apiserver/plane/app/urls/notification.py similarity index 98% rename from apiserver/plane/api/urls/notification.py rename to apiserver/plane/app/urls/notification.py index 5e1936d01..0c96e5f15 100644 --- a/apiserver/plane/api/urls/notification.py +++ b/apiserver/plane/app/urls/notification.py @@ -1,7 +1,7 @@ from django.urls import path -from plane.api.views import ( +from plane.app.views import ( NotificationViewSet, UnreadNotificationEndpoint, MarkAllReadNotificationViewSet, diff --git a/apiserver/plane/api/urls/page.py b/apiserver/plane/app/urls/page.py similarity index 99% rename from apiserver/plane/api/urls/page.py rename to apiserver/plane/app/urls/page.py index 8b08dcc79..58cec2cd4 100644 --- a/apiserver/plane/api/urls/page.py +++ b/apiserver/plane/app/urls/page.py @@ -1,7 +1,7 @@ from django.urls import path -from plane.api.views import ( +from plane.app.views import ( PageViewSet, PageFavoriteViewSet, PageLogEndpoint, diff --git a/apiserver/plane/api/urls/project.py b/apiserver/plane/app/urls/project.py similarity index 85% rename from apiserver/plane/api/urls/project.py rename to apiserver/plane/app/urls/project.py index 83bb765e6..4f0771952 100644 --- a/apiserver/plane/api/urls/project.py +++ b/apiserver/plane/app/urls/project.py @@ -1,6 +1,6 @@ from django.urls import path -from plane.api.views import ( +from plane.app.views import ( ProjectViewSet, ProjectInvitationsViewset, ProjectMemberViewSet, @@ -10,8 +10,9 @@ from plane.api.views import ( ProjectUserViewsEndpoint, ProjectIdentifierEndpoint, ProjectFavoritesViewSet, - ProjectPublicCoverImagesEndpoint, UserProjectInvitationsViewset, + ProjectPublicCoverImagesEndpoint, + ProjectDeployBoardViewSet, ) @@ -147,4 +148,25 @@ urlpatterns = [ ProjectPublicCoverImagesEndpoint.as_view(), name="project-covers", ), + path( + "workspaces//projects//project-deploy-boards/", + ProjectDeployBoardViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="project-deploy-board", + ), + path( + "workspaces//projects//project-deploy-boards//", + ProjectDeployBoardViewSet.as_view( + { + "get": "retrieve", + "patch": "partial_update", + "delete": "destroy", + } + ), + name="project-deploy-board", + ), ] diff --git a/apiserver/plane/api/urls/search.py b/apiserver/plane/app/urls/search.py similarity index 93% rename from apiserver/plane/api/urls/search.py rename to apiserver/plane/app/urls/search.py index 282feb046..05a79994e 100644 --- a/apiserver/plane/api/urls/search.py +++ b/apiserver/plane/app/urls/search.py @@ -1,7 +1,7 @@ from django.urls import path -from plane.api.views import ( +from plane.app.views import ( GlobalSearchEndpoint, IssueSearchEndpoint, ) diff --git a/apiserver/plane/api/urls/state.py b/apiserver/plane/app/urls/state.py similarity index 95% rename from apiserver/plane/api/urls/state.py rename to apiserver/plane/app/urls/state.py index 94aa55f24..9fec70ea1 100644 --- a/apiserver/plane/api/urls/state.py +++ b/apiserver/plane/app/urls/state.py @@ -1,7 +1,7 @@ from django.urls import path -from plane.api.views import StateViewSet +from plane.app.views import StateViewSet urlpatterns = [ diff --git a/apiserver/plane/api/urls/user.py b/apiserver/plane/app/urls/user.py similarity index 98% rename from apiserver/plane/api/urls/user.py rename to apiserver/plane/app/urls/user.py index da794d59a..c958addad 100644 --- a/apiserver/plane/api/urls/user.py +++ b/apiserver/plane/app/urls/user.py @@ -1,6 +1,6 @@ from django.urls import path -from plane.api.views import ( +from plane.app.views import ( ## User UserEndpoint, UpdateUserOnBoardedEndpoint, diff --git a/apiserver/plane/api/urls/views.py b/apiserver/plane/app/urls/views.py similarity index 98% rename from apiserver/plane/api/urls/views.py rename to apiserver/plane/app/urls/views.py index 560855e80..36372c03a 100644 --- a/apiserver/plane/api/urls/views.py +++ b/apiserver/plane/app/urls/views.py @@ -1,7 +1,7 @@ from django.urls import path -from plane.api.views import ( +from plane.app.views import ( IssueViewViewSet, GlobalViewViewSet, GlobalViewIssuesViewSet, diff --git a/apiserver/plane/api/urls/webhook.py b/apiserver/plane/app/urls/webhook.py similarity index 95% rename from apiserver/plane/api/urls/webhook.py rename to apiserver/plane/app/urls/webhook.py index 74a8da759..16cc48be8 100644 --- a/apiserver/plane/api/urls/webhook.py +++ b/apiserver/plane/app/urls/webhook.py @@ -1,6 +1,6 @@ from django.urls import path -from plane.api.views import ( +from plane.app.views import ( WebhookEndpoint, WebhookLogsEndpoint, WebhookSecretRegenerateEndpoint, diff --git a/apiserver/plane/api/urls/workspace.py b/apiserver/plane/app/urls/workspace.py similarity index 99% rename from apiserver/plane/api/urls/workspace.py rename to apiserver/plane/app/urls/workspace.py index 64e558f10..739d17c55 100644 --- a/apiserver/plane/api/urls/workspace.py +++ b/apiserver/plane/app/urls/workspace.py @@ -1,7 +1,7 @@ from django.urls import path -from plane.api.views import ( +from plane.app.views import ( UserWorkspaceInvitationsViewSet, WorkSpaceViewSet, WorkspaceJoinEndpoint, diff --git a/apiserver/plane/api/urls_deprecated.py b/apiserver/plane/app/urls_deprecated.py similarity index 99% rename from apiserver/plane/api/urls_deprecated.py rename to apiserver/plane/app/urls_deprecated.py index 1f05675a2..c6e6183fa 100644 --- a/apiserver/plane/api/urls_deprecated.py +++ b/apiserver/plane/app/urls_deprecated.py @@ -4,7 +4,7 @@ from rest_framework_simplejwt.views import TokenRefreshView # Create your urls here. -from plane.api.views import ( +from plane.app.views import ( # Authentication SignUpEndpoint, SignInEndpoint, diff --git a/apiserver/plane/api/views/__init__.py b/apiserver/plane/app/views/__init__.py similarity index 92% rename from apiserver/plane/api/views/__init__.py rename to apiserver/plane/app/views/__init__.py index 12b569523..f945f00e3 100644 --- a/apiserver/plane/api/views/__init__.py +++ b/apiserver/plane/app/views/__init__.py @@ -9,10 +9,8 @@ from .project import ( ProjectUserViewsEndpoint, ProjectMemberUserEndpoint, ProjectFavoritesViewSet, - ProjectDeployBoardViewSet, - ProjectDeployBoardPublicSettingsEndpoint, - WorkspaceProjectDeployBoardEndpoint, ProjectPublicCoverImagesEndpoint, + ProjectDeployBoardViewSet, ) from .user import ( UserEndpoint, @@ -80,15 +78,9 @@ from .issue import ( IssueAttachmentEndpoint, IssueArchiveViewSet, IssueSubscriberViewSet, - IssueCommentPublicViewSet, CommentReactionViewSet, IssueReactionViewSet, - IssueReactionPublicViewSet, - CommentReactionPublicViewSet, - IssueVotePublicViewSet, IssueRelationViewSet, - IssueRetrievePublicEndpoint, - ProjectIssuesPublicEndpoint, IssueDraftViewSet, ) @@ -156,7 +148,7 @@ from .estimate import ( BulkEstimatePointEndpoint, ) -from .inbox import InboxViewSet, InboxIssueViewSet, InboxIssuePublicViewSet +from .inbox import InboxViewSet, InboxIssueViewSet from .analytic import ( AnalyticsEndpoint, diff --git a/apiserver/plane/api/views/analytic.py b/apiserver/plane/app/views/analytic.py similarity index 98% rename from apiserver/plane/api/views/analytic.py rename to apiserver/plane/app/views/analytic.py index c29a4b692..c1deb0d8f 100644 --- a/apiserver/plane/api/views/analytic.py +++ b/apiserver/plane/app/views/analytic.py @@ -5,13 +5,12 @@ from django.db.models.functions import ExtractMonth # Third party imports from rest_framework import status from rest_framework.response import Response -from sentry_sdk import capture_exception # Module imports -from plane.api.views import BaseAPIView, BaseViewSet -from plane.api.permissions import WorkSpaceAdminPermission +from plane.app.views import BaseAPIView, BaseViewSet +from plane.app.permissions import WorkSpaceAdminPermission from plane.db.models import Issue, AnalyticView, Workspace, State, Label -from plane.api.serializers import AnalyticViewSerializer +from plane.app.serializers import AnalyticViewSerializer from plane.utils.analytics_plot import build_graph_plot from plane.bgtasks.analytic_plot_export import analytic_export_task from plane.utils.issue_filters import issue_filters diff --git a/apiserver/plane/api/views/api.py b/apiserver/plane/app/views/api.py similarity index 95% rename from apiserver/plane/api/views/api.py rename to apiserver/plane/app/views/api.py index 59da6d3c4..ce2d4bd09 100644 --- a/apiserver/plane/api/views/api.py +++ b/apiserver/plane/app/views/api.py @@ -8,8 +8,8 @@ from rest_framework import status # Module import from .base import BaseAPIView from plane.db.models import APIToken, Workspace -from plane.api.serializers import APITokenSerializer, APITokenReadSerializer -from plane.api.permissions import WorkspaceOwnerPermission +from plane.app.serializers import APITokenSerializer, APITokenReadSerializer +from plane.app.permissions import WorkspaceOwnerPermission class ApiTokenEndpoint(BaseAPIView): diff --git a/apiserver/plane/api/views/asset.py b/apiserver/plane/app/views/asset.py similarity index 95% rename from apiserver/plane/api/views/asset.py rename to apiserver/plane/app/views/asset.py index 3f5dcceac..eddbb4505 100644 --- a/apiserver/plane/api/views/asset.py +++ b/apiserver/plane/app/views/asset.py @@ -2,12 +2,11 @@ from rest_framework import status from rest_framework.response import Response from rest_framework.parsers import MultiPartParser, FormParser -from sentry_sdk import capture_exception -from django.conf import settings + # Module imports from .base import BaseAPIView from plane.db.models import FileAsset, Workspace -from plane.api.serializers import FileAssetSerializer +from plane.app.serializers import FileAssetSerializer class FileAssetEndpoint(BaseAPIView): diff --git a/apiserver/plane/api/views/auth_extended.py b/apiserver/plane/app/views/auth_extended.py similarity index 99% rename from apiserver/plane/api/views/auth_extended.py rename to apiserver/plane/app/views/auth_extended.py index e2ec9d5b6..5abd696fe 100644 --- a/apiserver/plane/api/views/auth_extended.py +++ b/apiserver/plane/app/views/auth_extended.py @@ -21,7 +21,7 @@ from sentry_sdk import capture_exception ## Module imports from . import BaseAPIView -from plane.api.serializers import ( +from plane.app.serializers import ( ChangePasswordSerializer, ResetPasswordSerializer, ) diff --git a/apiserver/plane/api/views/authentication.py b/apiserver/plane/app/views/authentication.py similarity index 99% rename from apiserver/plane/api/views/authentication.py rename to apiserver/plane/app/views/authentication.py index 2ec241303..93d381117 100644 --- a/apiserver/plane/api/views/authentication.py +++ b/apiserver/plane/app/views/authentication.py @@ -5,6 +5,7 @@ import string import json import requests from requests.exceptions import RequestException + # Django imports from django.utils import timezone from django.core.exceptions import ValidationError diff --git a/apiserver/plane/api/views/base.py b/apiserver/plane/app/views/base.py similarity index 100% rename from apiserver/plane/api/views/base.py rename to apiserver/plane/app/views/base.py diff --git a/apiserver/plane/api/views/config.py b/apiserver/plane/app/views/config.py similarity index 98% rename from apiserver/plane/api/views/config.py rename to apiserver/plane/app/views/config.py index 237d8d6bf..4a6b05859 100644 --- a/apiserver/plane/api/views/config.py +++ b/apiserver/plane/app/views/config.py @@ -8,7 +8,6 @@ from django.conf import settings from rest_framework.permissions import AllowAny from rest_framework import status from rest_framework.response import Response -from sentry_sdk import capture_exception # Module imports from .base import BaseAPIView diff --git a/apiserver/plane/api/views/cycle.py b/apiserver/plane/app/views/cycle.py similarity index 99% rename from apiserver/plane/api/views/cycle.py rename to apiserver/plane/app/views/cycle.py index 06df22077..a590dc214 100644 --- a/apiserver/plane/api/views/cycle.py +++ b/apiserver/plane/app/views/cycle.py @@ -20,18 +20,17 @@ from django.views.decorators.gzip import gzip_page # Third party imports from rest_framework.response import Response from rest_framework import status -from sentry_sdk import capture_exception # Module imports from . import BaseViewSet, BaseAPIView, WebhookMixin -from plane.api.serializers import ( +from plane.app.serializers import ( CycleSerializer, CycleIssueSerializer, CycleFavoriteSerializer, IssueStateSerializer, CycleWriteSerializer, ) -from plane.api.permissions import ProjectEntityPermission +from plane.app.permissions import ProjectEntityPermission from plane.db.models import ( User, Cycle, diff --git a/apiserver/plane/api/views/estimate.py b/apiserver/plane/app/views/estimate.py similarity index 97% rename from apiserver/plane/api/views/estimate.py rename to apiserver/plane/app/views/estimate.py index 3c2cca4d5..ec9393f5b 100644 --- a/apiserver/plane/api/views/estimate.py +++ b/apiserver/plane/app/views/estimate.py @@ -1,13 +1,12 @@ # Third party imports from rest_framework.response import Response from rest_framework import status -from sentry_sdk import capture_exception # Module imports from .base import BaseViewSet, BaseAPIView -from plane.api.permissions import ProjectEntityPermission +from plane.app.permissions import ProjectEntityPermission from plane.db.models import Project, Estimate, EstimatePoint -from plane.api.serializers import ( +from plane.app.serializers import ( EstimateSerializer, EstimatePointSerializer, EstimateReadSerializer, diff --git a/apiserver/plane/api/views/exporter.py b/apiserver/plane/app/views/exporter.py similarity index 94% rename from apiserver/plane/api/views/exporter.py rename to apiserver/plane/app/views/exporter.py index 03da8932f..b709a599d 100644 --- a/apiserver/plane/api/views/exporter.py +++ b/apiserver/plane/app/views/exporter.py @@ -1,15 +1,14 @@ # Third Party imports from rest_framework.response import Response from rest_framework import status -from sentry_sdk import capture_exception # Module imports from . import BaseAPIView -from plane.api.permissions import WorkSpaceAdminPermission +from plane.app.permissions import WorkSpaceAdminPermission from plane.bgtasks.export_task import issue_export_task from plane.db.models import Project, ExporterHistory, Workspace -from plane.api.serializers import ExporterHistorySerializer +from plane.app.serializers import ExporterHistorySerializer class ExportIssuesEndpoint(BaseAPIView): diff --git a/apiserver/plane/api/views/external.py b/apiserver/plane/app/views/external.py similarity index 94% rename from apiserver/plane/api/views/external.py rename to apiserver/plane/app/views/external.py index 1953743a2..ac502c186 100644 --- a/apiserver/plane/api/views/external.py +++ b/apiserver/plane/app/views/external.py @@ -5,17 +5,15 @@ import requests from openai import OpenAI from rest_framework.response import Response from rest_framework import status -from rest_framework.permissions import AllowAny -from sentry_sdk import capture_exception # Django imports from django.conf import settings # Module imports from .base import BaseAPIView -from plane.api.permissions import ProjectEntityPermission +from plane.app.permissions import ProjectEntityPermission from plane.db.models import Workspace, Project -from plane.api.serializers import ProjectLiteSerializer, WorkspaceLiteSerializer +from plane.app.serializers import ProjectLiteSerializer, WorkspaceLiteSerializer from plane.utils.integrations.github import get_release_notes from plane.license.models import InstanceConfiguration from plane.license.utils.instance_value import get_configuration_value diff --git a/apiserver/plane/api/views/importer.py b/apiserver/plane/app/views/importer.py similarity index 99% rename from apiserver/plane/api/views/importer.py rename to apiserver/plane/app/views/importer.py index 4060b2bd5..b99d663e2 100644 --- a/apiserver/plane/api/views/importer.py +++ b/apiserver/plane/app/views/importer.py @@ -4,13 +4,12 @@ import uuid # Third party imports from rest_framework import status from rest_framework.response import Response -from sentry_sdk import capture_exception # Django imports from django.db.models import Max, Q # Module imports -from plane.api.views import BaseAPIView +from plane.app.views import BaseAPIView from plane.db.models import ( WorkspaceIntegration, Importer, @@ -30,7 +29,7 @@ from plane.db.models import ( ModuleIssue, Label, ) -from plane.api.serializers import ( +from plane.app.serializers import ( ImporterSerializer, IssueFlatSerializer, ModuleSerializer, @@ -39,7 +38,7 @@ from plane.utils.integrations.github import get_github_repo_details from plane.utils.importers.jira import jira_project_issue_summary from plane.bgtasks.importer_task import service_importer from plane.utils.html_processor import strip_tags -from plane.api.permissions import WorkSpaceAdminPermission +from plane.app.permissions import WorkSpaceAdminPermission class ServiceIssueImportSummaryEndpoint(BaseAPIView): diff --git a/apiserver/plane/api/views/inbox.py b/apiserver/plane/app/views/inbox.py similarity index 56% rename from apiserver/plane/api/views/inbox.py rename to apiserver/plane/app/views/inbox.py index 999d0a459..38c0808b5 100644 --- a/apiserver/plane/api/views/inbox.py +++ b/apiserver/plane/app/views/inbox.py @@ -9,11 +9,10 @@ from django.core.serializers.json import DjangoJSONEncoder # Third party imports from rest_framework import status from rest_framework.response import Response -from sentry_sdk import capture_exception # Module imports from .base import BaseViewSet -from plane.api.permissions import ProjectBasePermission, ProjectLitePermission +from plane.app.permissions import ProjectBasePermission, ProjectLitePermission from plane.db.models import ( Inbox, InboxIssue, @@ -22,9 +21,8 @@ from plane.db.models import ( IssueLink, IssueAttachment, ProjectMember, - ProjectDeployBoard, ) -from plane.api.serializers import ( +from plane.app.serializers import ( IssueSerializer, InboxSerializer, InboxIssueSerializer, @@ -359,253 +357,3 @@ class InboxIssueViewSet(BaseViewSet): return Response(status=status.HTTP_204_NO_CONTENT) -class InboxIssuePublicViewSet(BaseViewSet): - serializer_class = InboxIssueSerializer - model = InboxIssue - - filterset_fields = [ - "status", - ] - - def get_queryset(self): - project_deploy_board = ProjectDeployBoard.objects.get( - workspace__slug=self.kwargs.get("slug"), - project_id=self.kwargs.get("project_id"), - ) - if project_deploy_board is not None: - return self.filter_queryset( - super() - .get_queryset() - .filter( - Q(snoozed_till__gte=timezone.now()) | Q(snoozed_till__isnull=True), - project_id=self.kwargs.get("project_id"), - workspace__slug=self.kwargs.get("slug"), - inbox_id=self.kwargs.get("inbox_id"), - ) - .select_related("issue", "workspace", "project") - ) - return InboxIssue.objects.none() - - def list(self, request, slug, project_id, inbox_id): - project_deploy_board = ProjectDeployBoard.objects.get( - workspace__slug=slug, project_id=project_id - ) - if project_deploy_board.inbox is None: - return Response( - {"error": "Inbox is not enabled for this Project Board"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - filters = issue_filters(request.query_params, "GET") - issues = ( - Issue.objects.filter( - issue_inbox__inbox_id=inbox_id, - workspace__slug=slug, - project_id=project_id, - ) - .filter(**filters) - .annotate(bridge_id=F("issue_inbox__id")) - .select_related("workspace", "project", "state", "parent") - .prefetch_related("assignees", "labels") - .order_by("issue_inbox__snoozed_till", "issue_inbox__status") - .annotate( - sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id")) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .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") - ) - .prefetch_related( - Prefetch( - "issue_inbox", - queryset=InboxIssue.objects.only( - "status", "duplicate_to", "snoozed_till", "source" - ), - ) - ) - ) - issues_data = IssueStateInboxSerializer(issues, many=True).data - return Response( - issues_data, - status=status.HTTP_200_OK, - ) - - def create(self, request, slug, project_id, inbox_id): - project_deploy_board = ProjectDeployBoard.objects.get( - workspace__slug=slug, project_id=project_id - ) - if project_deploy_board.inbox is None: - return Response( - {"error": "Inbox is not enabled for this Project Board"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - if not request.data.get("issue", {}).get("name", False): - return Response( - {"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST - ) - - # Check for valid priority - if not request.data.get("issue", {}).get("priority", "none") in [ - "low", - "medium", - "high", - "urgent", - "none", - ]: - return Response( - {"error": "Invalid priority"}, status=status.HTTP_400_BAD_REQUEST - ) - - # Create or get state - state, _ = State.objects.get_or_create( - name="Triage", - group="backlog", - description="Default state for managing all Inbox Issues", - project_id=project_id, - color="#ff7700", - ) - - # create an issue - issue = Issue.objects.create( - name=request.data.get("issue", {}).get("name"), - description=request.data.get("issue", {}).get("description", {}), - description_html=request.data.get("issue", {}).get( - "description_html", "

" - ), - priority=request.data.get("issue", {}).get("priority", "low"), - project_id=project_id, - state=state, - ) - - # Create an Issue Activity - issue_activity.delay( - type="issue.activity.created", - requested_data=json.dumps(request.data, cls=DjangoJSONEncoder), - actor_id=str(request.user.id), - issue_id=str(issue.id), - project_id=str(project_id), - current_instance=None, - epoch=int(timezone.now().timestamp()), - ) - # create an inbox issue - InboxIssue.objects.create( - inbox_id=inbox_id, - project_id=project_id, - issue=issue, - source=request.data.get("source", "in-app"), - ) - - serializer = IssueStateInboxSerializer(issue) - return Response(serializer.data, status=status.HTTP_200_OK) - - def partial_update(self, request, slug, project_id, inbox_id, pk): - project_deploy_board = ProjectDeployBoard.objects.get( - workspace__slug=slug, project_id=project_id - ) - if project_deploy_board.inbox is None: - return Response( - {"error": "Inbox is not enabled for this Project Board"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - inbox_issue = InboxIssue.objects.get( - pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id - ) - # Get the project member - if str(inbox_issue.created_by_id) != str(request.user.id): - return Response( - {"error": "You cannot edit inbox issues"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - # Get issue data - issue_data = request.data.pop("issue", False) - - issue = Issue.objects.get( - pk=inbox_issue.issue_id, workspace__slug=slug, project_id=project_id - ) - # viewers and guests since only viewers and guests - issue_data = { - "name": issue_data.get("name", issue.name), - "description_html": issue_data.get( - "description_html", issue.description_html - ), - "description": issue_data.get("description", issue.description), - } - - issue_serializer = IssueCreateSerializer(issue, data=issue_data, partial=True) - - if issue_serializer.is_valid(): - current_instance = issue - # Log all the updates - requested_data = json.dumps(issue_data, cls=DjangoJSONEncoder) - if issue is not None: - issue_activity.delay( - type="issue.activity.updated", - requested_data=requested_data, - actor_id=str(request.user.id), - issue_id=str(issue.id), - project_id=str(project_id), - current_instance=json.dumps( - IssueSerializer(current_instance).data, - cls=DjangoJSONEncoder, - ), - epoch=int(timezone.now().timestamp()), - ) - issue_serializer.save() - return Response(issue_serializer.data, status=status.HTTP_200_OK) - return Response(issue_serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - def retrieve(self, request, slug, project_id, inbox_id, pk): - project_deploy_board = ProjectDeployBoard.objects.get( - workspace__slug=slug, project_id=project_id - ) - if project_deploy_board.inbox is None: - return Response( - {"error": "Inbox is not enabled for this Project Board"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - inbox_issue = InboxIssue.objects.get( - pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id - ) - issue = Issue.objects.get( - pk=inbox_issue.issue_id, workspace__slug=slug, project_id=project_id - ) - serializer = IssueStateInboxSerializer(issue) - return Response(serializer.data, status=status.HTTP_200_OK) - - def destroy(self, request, slug, project_id, inbox_id, pk): - project_deploy_board = ProjectDeployBoard.objects.get( - workspace__slug=slug, project_id=project_id - ) - if project_deploy_board.inbox is None: - return Response( - {"error": "Inbox is not enabled for this Project Board"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - inbox_issue = InboxIssue.objects.get( - pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id - ) - - if str(inbox_issue.created_by_id) != str(request.user.id): - return Response( - {"error": "You cannot delete inbox issue"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - inbox_issue.delete() - return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/api/views/integration/__init__.py b/apiserver/plane/app/views/integration/__init__.py similarity index 100% rename from apiserver/plane/api/views/integration/__init__.py rename to apiserver/plane/app/views/integration/__init__.py diff --git a/apiserver/plane/api/views/integration/base.py b/apiserver/plane/app/views/integration/base.py similarity index 97% rename from apiserver/plane/api/views/integration/base.py rename to apiserver/plane/app/views/integration/base.py index cc911b537..b82957dfb 100644 --- a/apiserver/plane/api/views/integration/base.py +++ b/apiserver/plane/app/views/integration/base.py @@ -10,7 +10,7 @@ from rest_framework import status from sentry_sdk import capture_exception # Module imports -from plane.api.views import BaseViewSet +from plane.app.views import BaseViewSet from plane.db.models import ( Integration, WorkspaceIntegration, @@ -19,12 +19,12 @@ from plane.db.models import ( WorkspaceMember, APIToken, ) -from plane.api.serializers import IntegrationSerializer, WorkspaceIntegrationSerializer +from plane.app.serializers import IntegrationSerializer, WorkspaceIntegrationSerializer from plane.utils.integrations.github import ( get_github_metadata, delete_github_installation, ) -from plane.api.permissions import WorkSpaceAdminPermission +from plane.app.permissions import WorkSpaceAdminPermission from plane.utils.integrations.slack import slack_oauth class IntegrationViewSet(BaseViewSet): diff --git a/apiserver/plane/api/views/integration/github.py b/apiserver/plane/app/views/integration/github.py similarity index 97% rename from apiserver/plane/api/views/integration/github.py rename to apiserver/plane/app/views/integration/github.py index f2035639e..29b7a9b2f 100644 --- a/apiserver/plane/api/views/integration/github.py +++ b/apiserver/plane/app/views/integration/github.py @@ -4,7 +4,7 @@ from rest_framework.response import Response from sentry_sdk import capture_exception # Module imports -from plane.api.views import BaseViewSet, BaseAPIView +from plane.app.views import BaseViewSet, BaseAPIView from plane.db.models import ( GithubIssueSync, GithubRepositorySync, @@ -15,13 +15,13 @@ from plane.db.models import ( GithubCommentSync, Project, ) -from plane.api.serializers import ( +from plane.app.serializers import ( GithubIssueSyncSerializer, GithubRepositorySyncSerializer, GithubCommentSyncSerializer, ) from plane.utils.integrations.github import get_github_repos -from plane.api.permissions import ProjectBasePermission, ProjectEntityPermission +from plane.app.permissions import ProjectBasePermission, ProjectEntityPermission class GithubRepositoriesEndpoint(BaseAPIView): diff --git a/apiserver/plane/api/views/integration/slack.py b/apiserver/plane/app/views/integration/slack.py similarity index 94% rename from apiserver/plane/api/views/integration/slack.py rename to apiserver/plane/app/views/integration/slack.py index 6b1b47d37..3f18a2ab2 100644 --- a/apiserver/plane/api/views/integration/slack.py +++ b/apiserver/plane/app/views/integration/slack.py @@ -7,10 +7,10 @@ from rest_framework.response import Response from sentry_sdk import capture_exception # Module imports -from plane.api.views import BaseViewSet, BaseAPIView +from plane.app.views import BaseViewSet, BaseAPIView from plane.db.models import SlackProjectSync, WorkspaceIntegration, ProjectMember -from plane.api.serializers import SlackProjectSyncSerializer -from plane.api.permissions import ProjectBasePermission, ProjectEntityPermission +from plane.app.serializers import SlackProjectSyncSerializer +from plane.app.permissions import ProjectBasePermission, ProjectEntityPermission from plane.utils.integrations.slack import slack_oauth diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/app/views/issue.py similarity index 73% rename from apiserver/plane/api/views/issue.py rename to apiserver/plane/app/views/issue.py index 072fabe0e..4f7883868 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/app/views/issue.py @@ -29,12 +29,10 @@ from django.db import IntegrityError from rest_framework.response import Response from rest_framework import status from rest_framework.parsers import MultiPartParser, FormParser -from rest_framework.permissions import AllowAny, IsAuthenticated -from sentry_sdk import capture_exception # Module imports from . import BaseViewSet, BaseAPIView, WebhookMixin -from plane.api.serializers import ( +from plane.app.serializers import ( IssueCreateSerializer, IssueActivitySerializer, IssueCommentSerializer, @@ -54,7 +52,7 @@ from plane.api.serializers import ( RelatedIssueSerializer, IssuePublicSerializer, ) -from plane.api.permissions import ( +from plane.app.permissions import ( ProjectEntityPermission, WorkSpaceAdminPermission, ProjectMemberPermission, @@ -1462,432 +1460,6 @@ class CommentReactionViewSet(BaseViewSet): return Response(status=status.HTTP_204_NO_CONTENT) -class IssueCommentPublicViewSet(BaseViewSet): - serializer_class = IssueCommentSerializer - model = IssueComment - - filterset_fields = [ - "issue__id", - "workspace__id", - ] - - def get_permissions(self): - if self.action in ["list", "retrieve"]: - self.permission_classes = [ - AllowAny, - ] - else: - self.permission_classes = [ - IsAuthenticated, - ] - - return super(IssueCommentPublicViewSet, self).get_permissions() - - def get_queryset(self): - try: - project_deploy_board = ProjectDeployBoard.objects.get( - workspace__slug=self.kwargs.get("slug"), - project_id=self.kwargs.get("project_id"), - ) - if project_deploy_board.comments: - return self.filter_queryset( - super() - .get_queryset() - .filter(workspace__slug=self.kwargs.get("slug")) - .filter(issue_id=self.kwargs.get("issue_id")) - .filter(access="EXTERNAL") - .select_related("project") - .select_related("workspace") - .select_related("issue") - .annotate( - is_member=Exists( - ProjectMember.objects.filter( - workspace__slug=self.kwargs.get("slug"), - project_id=self.kwargs.get("project_id"), - member_id=self.request.user.id, - is_active=True, - ) - ) - ) - .distinct() - ).order_by("created_at") - return IssueComment.objects.none() - except ProjectDeployBoard.DoesNotExist: - return IssueComment.objects.none() - - def create(self, request, slug, project_id, issue_id): - project_deploy_board = ProjectDeployBoard.objects.get( - workspace__slug=slug, project_id=project_id - ) - - if not project_deploy_board.comments: - return Response( - {"error": "Comments are not enabled for this project"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - serializer = IssueCommentSerializer(data=request.data) - if serializer.is_valid(): - serializer.save( - project_id=project_id, - issue_id=issue_id, - actor=request.user, - access="EXTERNAL", - ) - issue_activity.delay( - type="comment.activity.created", - requested_data=json.dumps(serializer.data, cls=DjangoJSONEncoder), - actor_id=str(request.user.id), - issue_id=str(issue_id), - project_id=str(project_id), - current_instance=None, - epoch=int(timezone.now().timestamp()), - ) - if not ProjectMember.objects.filter( - project_id=project_id, - member=request.user, - is_active=True, - ).exists(): - # Add the user for workspace tracking - _ = ProjectPublicMember.objects.get_or_create( - project_id=project_id, - member=request.user, - ) - - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - def partial_update(self, request, slug, project_id, issue_id, pk): - project_deploy_board = ProjectDeployBoard.objects.get( - workspace__slug=slug, project_id=project_id - ) - - if not project_deploy_board.comments: - return Response( - {"error": "Comments are not enabled for this project"}, - status=status.HTTP_400_BAD_REQUEST, - ) - comment = IssueComment.objects.get( - workspace__slug=slug, pk=pk, actor=request.user - ) - serializer = IssueCommentSerializer(comment, data=request.data, partial=True) - if serializer.is_valid(): - serializer.save() - issue_activity.delay( - type="comment.activity.updated", - requested_data=json.dumps(request.data, cls=DjangoJSONEncoder), - actor_id=str(request.user.id), - issue_id=str(issue_id), - project_id=str(project_id), - current_instance=json.dumps( - IssueCommentSerializer(comment).data, - cls=DjangoJSONEncoder, - ), - epoch=int(timezone.now().timestamp()), - ) - return Response(serializer.data, status=status.HTTP_200_OK) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - def destroy(self, request, slug, project_id, issue_id, pk): - project_deploy_board = ProjectDeployBoard.objects.get( - workspace__slug=slug, project_id=project_id - ) - - if not project_deploy_board.comments: - return Response( - {"error": "Comments are not enabled for this project"}, - status=status.HTTP_400_BAD_REQUEST, - ) - comment = IssueComment.objects.get( - workspace__slug=slug, pk=pk, project_id=project_id, actor=request.user - ) - issue_activity.delay( - type="comment.activity.deleted", - requested_data=json.dumps({"comment_id": str(pk)}), - actor_id=str(request.user.id), - issue_id=str(issue_id), - project_id=str(project_id), - current_instance=json.dumps( - IssueCommentSerializer(comment).data, - cls=DjangoJSONEncoder, - ), - epoch=int(timezone.now().timestamp()), - ) - comment.delete() - return Response(status=status.HTTP_204_NO_CONTENT) - - -class IssueReactionPublicViewSet(BaseViewSet): - serializer_class = IssueReactionSerializer - model = IssueReaction - - def get_queryset(self): - try: - project_deploy_board = ProjectDeployBoard.objects.get( - workspace__slug=self.kwargs.get("slug"), - project_id=self.kwargs.get("project_id"), - ) - if project_deploy_board.reactions: - return ( - super() - .get_queryset() - .filter(workspace__slug=self.kwargs.get("slug")) - .filter(project_id=self.kwargs.get("project_id")) - .filter(issue_id=self.kwargs.get("issue_id")) - .order_by("-created_at") - .distinct() - ) - return IssueReaction.objects.none() - except ProjectDeployBoard.DoesNotExist: - return IssueReaction.objects.none() - - def create(self, request, slug, project_id, issue_id): - project_deploy_board = ProjectDeployBoard.objects.get( - workspace__slug=slug, project_id=project_id - ) - - if not project_deploy_board.reactions: - return Response( - {"error": "Reactions are not enabled for this project board"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - serializer = IssueReactionSerializer(data=request.data) - if serializer.is_valid(): - serializer.save( - project_id=project_id, issue_id=issue_id, actor=request.user - ) - if not ProjectMember.objects.filter( - project_id=project_id, - member=request.user, - is_active=True, - ).exists(): - # Add the user for workspace tracking - _ = ProjectPublicMember.objects.get_or_create( - project_id=project_id, - member=request.user, - ) - issue_activity.delay( - type="issue_reaction.activity.created", - requested_data=json.dumps(self.request.data, cls=DjangoJSONEncoder), - actor_id=str(self.request.user.id), - issue_id=str(self.kwargs.get("issue_id", None)), - project_id=str(self.kwargs.get("project_id", None)), - current_instance=None, - epoch=int(timezone.now().timestamp()), - ) - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - def destroy(self, request, slug, project_id, issue_id, reaction_code): - project_deploy_board = ProjectDeployBoard.objects.get( - workspace__slug=slug, project_id=project_id - ) - - if not project_deploy_board.reactions: - return Response( - {"error": "Reactions are not enabled for this project board"}, - status=status.HTTP_400_BAD_REQUEST, - ) - issue_reaction = IssueReaction.objects.get( - workspace__slug=slug, - issue_id=issue_id, - reaction=reaction_code, - actor=request.user, - ) - issue_activity.delay( - type="issue_reaction.activity.deleted", - requested_data=None, - actor_id=str(self.request.user.id), - issue_id=str(self.kwargs.get("issue_id", None)), - project_id=str(self.kwargs.get("project_id", None)), - current_instance=json.dumps( - { - "reaction": str(reaction_code), - "identifier": str(issue_reaction.id), - } - ), - epoch=int(timezone.now().timestamp()), - ) - issue_reaction.delete() - return Response(status=status.HTTP_204_NO_CONTENT) - - -class CommentReactionPublicViewSet(BaseViewSet): - serializer_class = CommentReactionSerializer - model = CommentReaction - - def get_queryset(self): - try: - project_deploy_board = ProjectDeployBoard.objects.get( - workspace__slug=self.kwargs.get("slug"), - project_id=self.kwargs.get("project_id"), - ) - if project_deploy_board.reactions: - return ( - super() - .get_queryset() - .filter(workspace__slug=self.kwargs.get("slug")) - .filter(project_id=self.kwargs.get("project_id")) - .filter(comment_id=self.kwargs.get("comment_id")) - .order_by("-created_at") - .distinct() - ) - return CommentReaction.objects.none() - except ProjectDeployBoard.DoesNotExist: - return CommentReaction.objects.none() - - def create(self, request, slug, project_id, comment_id): - project_deploy_board = ProjectDeployBoard.objects.get( - workspace__slug=slug, project_id=project_id - ) - - if not project_deploy_board.reactions: - return Response( - {"error": "Reactions are not enabled for this board"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - serializer = CommentReactionSerializer(data=request.data) - if serializer.is_valid(): - serializer.save( - project_id=project_id, comment_id=comment_id, actor=request.user - ) - if not ProjectMember.objects.filter( - project_id=project_id, - member=request.user, - is_active=True, - ).exists(): - # Add the user for workspace tracking - _ = ProjectPublicMember.objects.get_or_create( - project_id=project_id, - member=request.user, - ) - issue_activity.delay( - type="comment_reaction.activity.created", - requested_data=json.dumps(self.request.data, cls=DjangoJSONEncoder), - actor_id=str(self.request.user.id), - issue_id=None, - project_id=str(self.kwargs.get("project_id", None)), - current_instance=None, - epoch=int(timezone.now().timestamp()), - ) - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - def destroy(self, request, slug, project_id, comment_id, reaction_code): - project_deploy_board = ProjectDeployBoard.objects.get( - workspace__slug=slug, project_id=project_id - ) - if not project_deploy_board.reactions: - return Response( - {"error": "Reactions are not enabled for this board"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - comment_reaction = CommentReaction.objects.get( - project_id=project_id, - workspace__slug=slug, - comment_id=comment_id, - reaction=reaction_code, - actor=request.user, - ) - issue_activity.delay( - type="comment_reaction.activity.deleted", - requested_data=None, - actor_id=str(self.request.user.id), - issue_id=None, - project_id=str(self.kwargs.get("project_id", None)), - current_instance=json.dumps( - { - "reaction": str(reaction_code), - "identifier": str(comment_reaction.id), - "comment_id": str(comment_id), - } - ), - epoch=int(timezone.now().timestamp()), - ) - comment_reaction.delete() - return Response(status=status.HTTP_204_NO_CONTENT) - - -class IssueVotePublicViewSet(BaseViewSet): - model = IssueVote - serializer_class = IssueVoteSerializer - - def get_queryset(self): - try: - project_deploy_board = ProjectDeployBoard.objects.get( - workspace__slug=self.kwargs.get("slug"), - project_id=self.kwargs.get("project_id"), - ) - if project_deploy_board.votes: - return ( - super() - .get_queryset() - .filter(issue_id=self.kwargs.get("issue_id")) - .filter(workspace__slug=self.kwargs.get("slug")) - .filter(project_id=self.kwargs.get("project_id")) - ) - return IssueVote.objects.none() - except ProjectDeployBoard.DoesNotExist: - return IssueVote.objects.none() - - def create(self, request, slug, project_id, issue_id): - issue_vote, _ = IssueVote.objects.get_or_create( - actor_id=request.user.id, - project_id=project_id, - issue_id=issue_id, - ) - # Add the user for workspace tracking - if not ProjectMember.objects.filter( - project_id=project_id, - member=request.user, - is_active=True, - ).exists(): - _ = ProjectPublicMember.objects.get_or_create( - project_id=project_id, - member=request.user, - ) - issue_vote.vote = request.data.get("vote", 1) - issue_vote.save() - issue_activity.delay( - type="issue_vote.activity.created", - requested_data=json.dumps(self.request.data, cls=DjangoJSONEncoder), - actor_id=str(self.request.user.id), - issue_id=str(self.kwargs.get("issue_id", None)), - project_id=str(self.kwargs.get("project_id", None)), - current_instance=None, - epoch=int(timezone.now().timestamp()), - ) - serializer = IssueVoteSerializer(issue_vote) - return Response(serializer.data, status=status.HTTP_201_CREATED) - - def destroy(self, request, slug, project_id, issue_id): - issue_vote = IssueVote.objects.get( - workspace__slug=slug, - project_id=project_id, - issue_id=issue_id, - actor_id=request.user.id, - ) - issue_activity.delay( - type="issue_vote.activity.deleted", - requested_data=None, - actor_id=str(self.request.user.id), - issue_id=str(self.kwargs.get("issue_id", None)), - project_id=str(self.kwargs.get("project_id", None)), - current_instance=json.dumps( - { - "vote": str(issue_vote.vote), - "identifier": str(issue_vote.id), - } - ), - epoch=int(timezone.now().timestamp()), - ) - issue_vote.delete() - return Response(status=status.HTTP_204_NO_CONTENT) - - class IssueRelationViewSet(BaseViewSet): serializer_class = IssueRelationSerializer model = IssueRelation @@ -1973,182 +1545,6 @@ class IssueRelationViewSet(BaseViewSet): return Response(status=status.HTTP_204_NO_CONTENT) -class IssueRetrievePublicEndpoint(BaseAPIView): - permission_classes = [ - AllowAny, - ] - - def get(self, request, slug, project_id, issue_id): - issue = Issue.objects.get( - workspace__slug=slug, project_id=project_id, pk=issue_id - ) - serializer = IssuePublicSerializer(issue) - return Response(serializer.data, status=status.HTTP_200_OK) - - -class ProjectIssuesPublicEndpoint(BaseAPIView): - permission_classes = [ - AllowAny, - ] - - def get(self, request, slug, project_id): - 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"), - ) - ) - .prefetch_related( - Prefetch( - "votes", - queryset=IssueVote.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 - - state_group_order = [ - "backlog", - "unstarted", - "started", - "completed", - "cancelled", - ] - - states = ( - State.objects.filter( - ~Q(name="Triage"), - workspace__slug=slug, - project_id=project_id, - ) - .annotate( - custom_order=Case( - *[ - When(group=value, then=Value(index)) - for index, value in enumerate(state_group_order) - ], - default=Value(len(state_group_order)), - output_field=IntegerField(), - ), - ) - .values("name", "group", "color", "id") - .order_by("custom_order", "sequence") - ) - - 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, - ) - - class IssueDraftViewSet(BaseViewSet): permission_classes = [ ProjectEntityPermission, diff --git a/apiserver/plane/api/views/module.py b/apiserver/plane/app/views/module.py similarity index 99% rename from apiserver/plane/api/views/module.py rename to apiserver/plane/app/views/module.py index f8d74dbed..cc6369fe2 100644 --- a/apiserver/plane/api/views/module.py +++ b/apiserver/plane/app/views/module.py @@ -3,7 +3,6 @@ import json # Django Imports from django.utils import timezone -from django.db import IntegrityError from django.db.models import Prefetch, F, OuterRef, Func, Exists, Count, Q from django.core import serializers from django.utils.decorators import method_decorator @@ -12,11 +11,10 @@ from django.views.decorators.gzip import gzip_page # Third party imports from rest_framework.response import Response from rest_framework import status -from sentry_sdk import capture_exception # Module imports from . import BaseViewSet, BaseAPIView, WebhookMixin -from plane.api.serializers import ( +from plane.app.serializers import ( ModuleWriteSerializer, ModuleSerializer, ModuleIssueSerializer, @@ -24,7 +22,7 @@ from plane.api.serializers import ( ModuleFavoriteSerializer, IssueStateSerializer, ) -from plane.api.permissions import ProjectEntityPermission +from plane.app.permissions import ProjectEntityPermission from plane.db.models import ( Module, ModuleIssue, diff --git a/apiserver/plane/api/views/notification.py b/apiserver/plane/app/views/notification.py similarity index 99% rename from apiserver/plane/api/views/notification.py rename to apiserver/plane/app/views/notification.py index 19dcba734..9494ea86c 100644 --- a/apiserver/plane/api/views/notification.py +++ b/apiserver/plane/app/views/notification.py @@ -5,7 +5,6 @@ from django.utils import timezone # Third party imports from rest_framework import status from rest_framework.response import Response -from sentry_sdk import capture_exception from plane.utils.paginator import BasePaginator # Module imports @@ -17,7 +16,7 @@ from plane.db.models import ( Issue, WorkspaceMember, ) -from plane.api.serializers import NotificationSerializer +from plane.app.serializers import NotificationSerializer class NotificationViewSet(BaseViewSet, BasePaginator): diff --git a/apiserver/plane/api/views/oauth.py b/apiserver/plane/app/views/oauth.py similarity index 99% rename from apiserver/plane/api/views/oauth.py rename to apiserver/plane/app/views/oauth.py index d2b65d926..d7d9fe9e0 100644 --- a/apiserver/plane/api/views/oauth.py +++ b/apiserver/plane/app/views/oauth.py @@ -29,7 +29,6 @@ from plane.db.models import ( ProjectMemberInvite, ProjectMember, ) -from plane.api.serializers import UserSerializer from .base import BaseAPIView diff --git a/apiserver/plane/api/views/page.py b/apiserver/plane/app/views/page.py similarity index 90% rename from apiserver/plane/api/views/page.py rename to apiserver/plane/app/views/page.py index d8c90fc8f..b218b6687 100644 --- a/apiserver/plane/api/views/page.py +++ b/apiserver/plane/app/views/page.py @@ -3,26 +3,18 @@ from datetime import timedelta, date, datetime # Django imports from django.db import connection -from django.db.models import Exists, OuterRef, Q, Prefetch +from django.db.models import Exists, OuterRef, Q from django.utils import timezone from django.utils.decorators import method_decorator from django.views.decorators.gzip import gzip_page -from django.db.models import ( - OuterRef, - Func, - F, - Q, - Exists, -) # Third party imports from rest_framework import status from rest_framework.response import Response -from sentry_sdk import capture_exception # Module imports from .base import BaseViewSet, BaseAPIView -from plane.api.permissions import ProjectEntityPermission +from plane.app.permissions import ProjectEntityPermission from plane.db.models import ( Page, PageFavorite, @@ -31,7 +23,7 @@ from plane.db.models import ( IssueActivity, PageLog, ) -from plane.api.serializers import ( +from plane.app.serializers import ( PageSerializer, PageFavoriteSerializer, PageLogSerializer, @@ -87,15 +79,10 @@ class PageViewSet(BaseViewSet): .annotate(is_favorite=Exists(subquery)) .order_by(self.request.GET.get("order_by", "-created_at")) .prefetch_related("labels") - .order_by("-is_favorite","-created_at") + .order_by("-is_favorite", "-created_at") .distinct() ) - def perform_create(self, serializer): - serializer.save( - project_id=self.kwargs.get("project_id"), owned_by=self.request.user - ) - def create(self, request, slug, project_id): serializer = PageSerializer( data=request.data, @@ -148,10 +135,8 @@ class PageViewSet(BaseViewSet): status=status.HTTP_400_BAD_REQUEST, ) - def lock(self, request, slug, project_id, pk): - page = Page.objects.filter( - pk=pk, workspace__slug=slug, project_id=project_id - ) + def lock(self, request, slug, project_id, page_id): + page = Page.objects.get(pk=page_id, workspace__slug=slug, project_id=project_id) # only the owner can lock the page if request.user.id != page.owned_by_id: @@ -163,8 +148,8 @@ class PageViewSet(BaseViewSet): page.save() return Response(status=status.HTTP_204_NO_CONTENT) - def unlock(self, request, slug, project_id, pk): - page = Page.objects.get(pk=pk, workspace__slug=slug, project_id=project_id) + def unlock(self, request, slug, project_id, page_id): + page = Page.objects.get(pk=page_id, workspace__slug=slug, project_id=project_id) # only the owner can unlock the page if request.user.id != page.owned_by_id: @@ -242,27 +227,31 @@ class PageViewSet(BaseViewSet): ) def archive(self, request, slug, project_id, page_id): - _ = Page.objects.get( - project_id=project_id, - owned_by_id=request.user.id, - workspace__slug=slug, - pk=page_id, - ) + page = Page.objects.get(pk=page_id, workspace__slug=slug, project_id=project_id) + + if page.owned_by_id != request.user.id: + return Response( + {"error": "Only the owner of the page can archive a page"}, + status=status.HTTP_204_NO_CONTENT, + ) unarchive_archive_page_and_descendants(page_id, datetime.now()) return Response(status=status.HTTP_204_NO_CONTENT) def unarchive(self, request, slug, project_id, page_id): - page = Page.objects.get( - project_id=project_id, - owned_by_id=request.user.id, - workspace__slug=slug, - pk=page_id, - ) + page = Page.objects.get(pk=page_id, workspace__slug=slug, project_id=project_id) - page.parent = None - page.save() + if page.owned_by_id != request.user.id: + return Response( + {"error": "Only the owner of the page can unarchive a page"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + # if parent page is archived then the page will be un archived breaking the hierarchy + if page.parent_id and page.parent.archived_at: + page.parent = None + page.save(update_fields=['parent']) unarchive_archive_page_and_descendants(page_id, None) @@ -275,20 +264,13 @@ class PageViewSet(BaseViewSet): workspace__slug=slug, ) .filter(archived_at__isnull=False) - .filter(parent_id__isnull=True) ) - if not pages: - return Response( - {"error": "No pages found"}, status=status.HTTP_400_BAD_REQUEST - ) - return Response( PageSerializer(pages, many=True).data, status=status.HTTP_200_OK ) - class PageFavoriteViewSet(BaseViewSet): permission_classes = [ ProjectEntityPermission, @@ -410,11 +392,9 @@ class SubPagesEndpoint(BaseAPIView): workspace__slug=slug, entity_name__in=["forward_link", "back_link"], ) - .filter(archived_at__isnull=True) .select_related("project") .select_related("workspace") ) return Response( SubPageSerializer(pages, many=True).data, status=status.HTTP_200_OK - ) - + ) \ No newline at end of file diff --git a/apiserver/plane/api/views/project.py b/apiserver/plane/app/views/project.py similarity index 96% rename from apiserver/plane/api/views/project.py rename to apiserver/plane/app/views/project.py index ce7750105..727aa06ba 100644 --- a/apiserver/plane/api/views/project.py +++ b/apiserver/plane/app/views/project.py @@ -27,7 +27,7 @@ from rest_framework.permissions import AllowAny # Module imports from .base import BaseViewSet, BaseAPIView, WebhookMixin -from plane.api.serializers import ( +from plane.app.serializers import ( ProjectSerializer, ProjectListSerializer, ProjectMemberSerializer, @@ -38,12 +38,9 @@ from plane.api.serializers import ( ProjectMemberAdminSerializer, ) -from plane.api.permissions import ( - WorkspaceUserPermission, +from plane.app.permissions import ( ProjectBasePermission, - ProjectEntityPermission, ProjectMemberPermission, - ProjectLitePermission, ) from plane.db.models import ( @@ -965,6 +962,37 @@ class ProjectFavoritesViewSet(BaseViewSet): return Response(status=status.HTTP_204_NO_CONTENT) +class ProjectPublicCoverImagesEndpoint(BaseAPIView): + permission_classes = [ + AllowAny, + ] + + def get(self, request): + files = [] + s3 = boto3.client( + "s3", + aws_access_key_id=settings.AWS_ACCESS_KEY_ID, + aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY, + ) + params = { + "Bucket": settings.AWS_S3_BUCKET_NAME, + "Prefix": "static/project-cover/", + } + + response = s3.list_objects_v2(**params) + # Extracting file keys from the response + if "Contents" in response: + for content in response["Contents"]: + if not content["Key"].endswith( + "/" + ): # This line ensures we're only getting files, not "sub-folders" + files.append( + f"https://{settings.AWS_S3_BUCKET_NAME}.s3.{settings.AWS_REGION}.amazonaws.com/{content['Key']}" + ) + + return Response(files, status=status.HTTP_200_OK) + + class ProjectDeployBoardViewSet(BaseViewSet): permission_classes = [ ProjectMemberPermission, @@ -1012,77 +1040,4 @@ class ProjectDeployBoardViewSet(BaseViewSet): project_deploy_board.save() serializer = ProjectDeployBoardSerializer(project_deploy_board) - return Response(serializer.data, status=status.HTTP_200_OK) - - -class ProjectDeployBoardPublicSettingsEndpoint(BaseAPIView): - permission_classes = [ - AllowAny, - ] - - def get(self, request, slug, project_id): - project_deploy_board = ProjectDeployBoard.objects.get( - workspace__slug=slug, project_id=project_id - ) - serializer = ProjectDeployBoardSerializer(project_deploy_board) - return Response(serializer.data, status=status.HTTP_200_OK) - - -class WorkspaceProjectDeployBoardEndpoint(BaseAPIView): - permission_classes = [ - AllowAny, - ] - - def get(self, request, slug): - projects = ( - Project.objects.filter(workspace__slug=slug) - .annotate( - is_public=Exists( - ProjectDeployBoard.objects.filter( - workspace__slug=slug, project_id=OuterRef("pk") - ) - ) - ) - .filter(is_public=True) - ).values( - "id", - "identifier", - "name", - "description", - "emoji", - "icon_prop", - "cover_image", - ) - - return Response(projects, status=status.HTTP_200_OK) - - -class ProjectPublicCoverImagesEndpoint(BaseAPIView): - permission_classes = [ - AllowAny, - ] - - def get(self, request): - files = [] - s3 = boto3.client( - "s3", - aws_access_key_id=settings.AWS_ACCESS_KEY_ID, - aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY, - ) - params = { - "Bucket": settings.AWS_S3_BUCKET_NAME, - "Prefix": "static/project-cover/", - } - - response = s3.list_objects_v2(**params) - # Extracting file keys from the response - if "Contents" in response: - for content in response["Contents"]: - if not content["Key"].endswith( - "/" - ): # This line ensures we're only getting files, not "sub-folders" - files.append( - f"https://{settings.AWS_S3_BUCKET_NAME}.s3.{settings.AWS_REGION}.amazonaws.com/{content['Key']}" - ) - - return Response(files, status=status.HTTP_200_OK) + return Response(serializer.data, status=status.HTTP_200_OK) \ No newline at end of file diff --git a/apiserver/plane/api/views/search.py b/apiserver/plane/app/views/search.py similarity index 99% rename from apiserver/plane/api/views/search.py rename to apiserver/plane/app/views/search.py index ff7431543..ac560643a 100644 --- a/apiserver/plane/api/views/search.py +++ b/apiserver/plane/app/views/search.py @@ -7,7 +7,6 @@ from django.db.models import Q # Third party imports from rest_framework import status from rest_framework.response import Response -from sentry_sdk import capture_exception # Module imports from .base import BaseAPIView diff --git a/apiserver/plane/api/views/state.py b/apiserver/plane/app/views/state.py similarity index 94% rename from apiserver/plane/api/views/state.py rename to apiserver/plane/app/views/state.py index dbb6e1d71..124bdf8fd 100644 --- a/apiserver/plane/api/views/state.py +++ b/apiserver/plane/app/views/state.py @@ -7,12 +7,11 @@ from django.db.models import Q # Third party imports from rest_framework.response import Response from rest_framework import status -from sentry_sdk import capture_exception # Module imports -from . import BaseViewSet, BaseAPIView -from plane.api.serializers import StateSerializer -from plane.api.permissions import ProjectEntityPermission +from . import BaseViewSet +from plane.app.serializers import StateSerializer +from plane.app.permissions import ProjectEntityPermission from plane.db.models import State, Issue diff --git a/apiserver/plane/api/views/user.py b/apiserver/plane/app/views/user.py similarity index 95% rename from apiserver/plane/api/views/user.py rename to apiserver/plane/app/views/user.py index e6e742a63..ed1178886 100644 --- a/apiserver/plane/api/views/user.py +++ b/apiserver/plane/app/views/user.py @@ -2,17 +2,16 @@ from rest_framework.response import Response from rest_framework import status -from sentry_sdk import capture_exception # Module imports -from plane.api.serializers import ( +from plane.app.serializers import ( UserSerializer, IssueActivitySerializer, UserMeSerializer, UserMeSettingsSerializer, ) -from plane.api.views.base import BaseViewSet, BaseAPIView +from plane.app.views.base import BaseViewSet, BaseAPIView from plane.db.models import User, IssueActivity, WorkspaceMember from plane.license.models import Instance, InstanceAdmin from plane.utils.paginator import BasePaginator diff --git a/apiserver/plane/api/views/view.py b/apiserver/plane/app/views/view.py similarity index 97% rename from apiserver/plane/api/views/view.py rename to apiserver/plane/app/views/view.py index f58f320b7..8e0e72f66 100644 --- a/apiserver/plane/api/views/view.py +++ b/apiserver/plane/app/views/view.py @@ -18,17 +18,16 @@ from django.db.models import Prefetch, OuterRef, Exists # Third party imports from rest_framework.response import Response from rest_framework import status -from sentry_sdk import capture_exception # Module imports -from . import BaseViewSet, BaseAPIView -from plane.api.serializers import ( +from . import BaseViewSet +from plane.app.serializers import ( GlobalViewSerializer, IssueViewSerializer, IssueLiteSerializer, IssueViewFavoriteSerializer, ) -from plane.api.permissions import WorkspaceEntityPermission, ProjectEntityPermission +from plane.app.permissions import WorkspaceEntityPermission, ProjectEntityPermission from plane.db.models import ( Workspace, GlobalView, diff --git a/apiserver/plane/api/views/webhook.py b/apiserver/plane/app/views/webhook.py similarity index 97% rename from apiserver/plane/api/views/webhook.py rename to apiserver/plane/app/views/webhook.py index 91a2f6729..74d23dd91 100644 --- a/apiserver/plane/api/views/webhook.py +++ b/apiserver/plane/app/views/webhook.py @@ -9,8 +9,8 @@ from rest_framework.response import Response from plane.db.models import Webhook, WebhookLog, Workspace from plane.db.models.webhook import generate_token from .base import BaseAPIView -from plane.api.permissions import WorkspaceOwnerPermission -from plane.api.serializers import WebhookSerializer, WebhookLogSerializer +from plane.app.permissions import WorkspaceOwnerPermission +from plane.app.serializers import WebhookSerializer, WebhookLogSerializer class WebhookEndpoint(BaseAPIView): diff --git a/apiserver/plane/api/views/workspace.py b/apiserver/plane/app/views/workspace.py similarity index 99% rename from apiserver/plane/api/views/workspace.py rename to apiserver/plane/app/views/workspace.py index 8804d48ef..637fc95b5 100644 --- a/apiserver/plane/api/views/workspace.py +++ b/apiserver/plane/app/views/workspace.py @@ -29,10 +29,10 @@ from django.db.models.fields import DateField # Third party modules from rest_framework import status from rest_framework.response import Response -from rest_framework.permissions import AllowAny, IsAuthenticated +from rest_framework.permissions import AllowAny # Module imports -from plane.api.serializers import ( +from plane.app.serializers import ( WorkSpaceSerializer, WorkSpaceMemberSerializer, TeamSerializer, @@ -45,7 +45,7 @@ from plane.api.serializers import ( WorkspaceMemberAdminSerializer, WorkspaceMemberMeSerializer, ) -from plane.api.views.base import BaseAPIView +from plane.app.views.base import BaseAPIView from . import BaseViewSet from plane.db.models import ( User, @@ -65,7 +65,7 @@ from plane.db.models import ( CycleIssue, IssueReaction, ) -from plane.api.permissions import ( +from plane.app.permissions import ( WorkSpaceBasePermission, WorkSpaceAdminPermission, WorkspaceEntityPermission, diff --git a/apiserver/plane/bgtasks/importer_task.py b/apiserver/plane/bgtasks/importer_task.py index f9e3df21e..84d10ecd3 100644 --- a/apiserver/plane/bgtasks/importer_task.py +++ b/apiserver/plane/bgtasks/importer_task.py @@ -13,7 +13,7 @@ from celery import shared_task from sentry_sdk import capture_exception # Module imports -from plane.api.serializers import ImporterSerializer +from plane.app.serializers import ImporterSerializer from plane.db.models import ( Importer, WorkspaceMember, diff --git a/apiserver/plane/bgtasks/issue_activites_task.py b/apiserver/plane/bgtasks/issue_activites_task.py index 4776bceab..3b2b40223 100644 --- a/apiserver/plane/bgtasks/issue_activites_task.py +++ b/apiserver/plane/bgtasks/issue_activites_task.py @@ -21,14 +21,11 @@ from plane.db.models import ( State, Cycle, Module, - IssueSubscriber, - Notification, - IssueAssignee, IssueReaction, CommentReaction, IssueComment, ) -from plane.api.serializers import IssueActivitySerializer +from plane.app.serializers import IssueActivitySerializer from plane.bgtasks.notification_task import notifications diff --git a/apiserver/plane/license/api/serializers/instance.py b/apiserver/plane/license/api/serializers/instance.py index b8c990522..f5cff760b 100644 --- a/apiserver/plane/license/api/serializers/instance.py +++ b/apiserver/plane/license/api/serializers/instance.py @@ -1,7 +1,7 @@ # Module imports from plane.license.models import Instance, InstanceAdmin, InstanceConfiguration -from plane.api.serializers import BaseSerializer -from plane.api.serializers import UserAdminLiteSerializer +from plane.app.serializers import BaseSerializer +from plane.app.serializers import UserAdminLiteSerializer class InstanceSerializer(BaseSerializer): diff --git a/apiserver/plane/license/api/views/instance.py b/apiserver/plane/license/api/views/instance.py index 309b2b9da..6b27419fd 100644 --- a/apiserver/plane/license/api/views/instance.py +++ b/apiserver/plane/license/api/views/instance.py @@ -11,7 +11,7 @@ from rest_framework import status from rest_framework.response import Response # Module imports -from plane.api.views import BaseAPIView +from plane.app.views import BaseAPIView from plane.license.models import Instance, InstanceAdmin, InstanceConfiguration from plane.license.api.serializers import ( InstanceSerializer, diff --git a/apiserver/plane/settings/common.py b/apiserver/plane/settings/common.py index f6359344d..2cf94a68c 100644 --- a/apiserver/plane/settings/common.py +++ b/apiserver/plane/settings/common.py @@ -34,7 +34,8 @@ INSTALLED_APPS = [ "django.contrib.sessions", # Inhouse apps "plane.analytics", - "plane.api", + "plane.app", + "plane.space", "plane.bgtasks", "plane.db", "plane.utils", diff --git a/apiserver/plane/space/__init__.py b/apiserver/plane/space/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apiserver/plane/space/apps.py b/apiserver/plane/space/apps.py new file mode 100644 index 000000000..6f1e76c51 --- /dev/null +++ b/apiserver/plane/space/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class SpaceConfig(AppConfig): + name = "plane.space" diff --git a/apiserver/plane/space/serializer/__init__.py b/apiserver/plane/space/serializer/__init__.py new file mode 100644 index 000000000..cd10fb5c6 --- /dev/null +++ b/apiserver/plane/space/serializer/__init__.py @@ -0,0 +1,5 @@ +from .user import UserLiteSerializer + +from .issue import LabelLiteSerializer, StateLiteSerializer + +from .state import StateSerializer, StateLiteSerializer diff --git a/apiserver/plane/space/serializer/base.py b/apiserver/plane/space/serializer/base.py new file mode 100644 index 000000000..89c9725d9 --- /dev/null +++ b/apiserver/plane/space/serializer/base.py @@ -0,0 +1,58 @@ +from rest_framework import serializers + + +class BaseSerializer(serializers.ModelSerializer): + id = serializers.PrimaryKeyRelatedField(read_only=True) + +class DynamicBaseSerializer(BaseSerializer): + + def __init__(self, *args, **kwargs): + # If 'fields' is provided in the arguments, remove it and store it separately. + # This is done so as not to pass this custom argument up to the superclass. + fields = kwargs.pop("fields", None) + + # Call the initialization of the superclass. + super().__init__(*args, **kwargs) + + # If 'fields' was provided, filter the fields of the serializer accordingly. + if fields is not None: + self.fields = self._filter_fields(fields) + + def _filter_fields(self, fields): + """ + Adjust the serializer's fields based on the provided 'fields' list. + + :param fields: List or dictionary specifying which fields to include in the serializer. + :return: The updated fields for the serializer. + """ + # Check each field_name in the provided fields. + for field_name in fields: + # If the field is a dictionary (indicating nested fields), + # loop through its keys and values. + if isinstance(field_name, dict): + for key, value in field_name.items(): + # If the value of this nested field is a list, + # perform a recursive filter on it. + if isinstance(value, list): + self._filter_fields(self.fields[key], value) + + # Create a list to store allowed fields. + allowed = [] + for item in fields: + # If the item is a string, it directly represents a field's name. + if isinstance(item, str): + allowed.append(item) + # If the item is a dictionary, it represents a nested field. + # Add the key of this dictionary to the allowed list. + elif isinstance(item, dict): + allowed.append(list(item.keys())[0]) + + # Convert the current serializer's fields and the allowed fields to sets. + existing = set(self.fields) + allowed = set(allowed) + + # Remove fields from the serializer that aren't in the 'allowed' list. + for field_name in (existing - allowed): + self.fields.pop(field_name) + + return self.fields diff --git a/apiserver/plane/space/serializer/cycle.py b/apiserver/plane/space/serializer/cycle.py new file mode 100644 index 000000000..ab4d9441d --- /dev/null +++ b/apiserver/plane/space/serializer/cycle.py @@ -0,0 +1,18 @@ +# Module imports +from .base import BaseSerializer +from plane.db.models import ( + Cycle, +) + +class CycleBaseSerializer(BaseSerializer): + class Meta: + model = Cycle + fields = "__all__" + read_only_fields = [ + "workspace", + "project", + "created_by", + "updated_by", + "created_at", + "updated_at", + ] \ No newline at end of file diff --git a/apiserver/plane/space/serializer/inbox.py b/apiserver/plane/space/serializer/inbox.py new file mode 100644 index 000000000..05d99ac55 --- /dev/null +++ b/apiserver/plane/space/serializer/inbox.py @@ -0,0 +1,47 @@ +# Third Party imports +from rest_framework import serializers + +# Module imports +from .base import BaseSerializer +from .user import UserLiteSerializer +from .state import StateLiteSerializer +from .project import ProjectLiteSerializer +from .issue import IssueFlatSerializer, LabelLiteSerializer +from plane.db.models import ( + Issue, + InboxIssue, +) + + +class InboxIssueSerializer(BaseSerializer): + issue_detail = IssueFlatSerializer(source="issue", read_only=True) + project_detail = ProjectLiteSerializer(source="project", read_only=True) + + class Meta: + model = InboxIssue + fields = "__all__" + read_only_fields = [ + "project", + "workspace", + ] + + +class InboxIssueLiteSerializer(BaseSerializer): + class Meta: + model = InboxIssue + fields = ["id", "status", "duplicate_to", "snoozed_till", "source"] + read_only_fields = fields + + +class IssueStateInboxSerializer(BaseSerializer): + state_detail = StateLiteSerializer(read_only=True, source="state") + project_detail = ProjectLiteSerializer(read_only=True, source="project") + label_details = LabelLiteSerializer(read_only=True, source="labels", many=True) + assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True) + sub_issues_count = serializers.IntegerField(read_only=True) + bridge_id = serializers.UUIDField(read_only=True) + issue_inbox = InboxIssueLiteSerializer(read_only=True, many=True) + + class Meta: + model = Issue + fields = "__all__" \ No newline at end of file diff --git a/apiserver/plane/space/serializer/issue.py b/apiserver/plane/space/serializer/issue.py new file mode 100644 index 000000000..1a9a872ef --- /dev/null +++ b/apiserver/plane/space/serializer/issue.py @@ -0,0 +1,506 @@ + +# Django imports +from django.utils import timezone + +# Third Party imports +from rest_framework import serializers + +# Module imports +from .base import BaseSerializer +from .user import UserLiteSerializer +from .state import StateSerializer, StateLiteSerializer +from .project import ProjectLiteSerializer +from .cycle import CycleBaseSerializer +from .module import ModuleBaseSerializer +from .workspace import WorkspaceLiteSerializer +from plane.db.models import ( + User, + Issue, + IssueComment, + IssueAssignee, + IssueLabel, + Label, + CycleIssue, + ModuleIssue, + IssueLink, + IssueAttachment, + IssueReaction, + CommentReaction, + IssueVote, + IssueRelation, +) + + +class IssueStateFlatSerializer(BaseSerializer): + state_detail = StateLiteSerializer(read_only=True, source="state") + project_detail = ProjectLiteSerializer(read_only=True, source="project") + + class Meta: + model = Issue + fields = [ + "id", + "sequence_id", + "name", + "state_detail", + "project_detail", + ] + + +class LabelSerializer(BaseSerializer): + workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True) + project_detail = ProjectLiteSerializer(source="project", read_only=True) + + class Meta: + model = Label + fields = "__all__" + read_only_fields = [ + "workspace", + "project", + ] + + +class IssueProjectLiteSerializer(BaseSerializer): + project_detail = ProjectLiteSerializer(source="project", read_only=True) + + class Meta: + model = Issue + fields = [ + "id", + "project_detail", + "name", + "sequence_id", + ] + read_only_fields = fields + + +class IssueRelationSerializer(BaseSerializer): + issue_detail = IssueProjectLiteSerializer(read_only=True, source="related_issue") + + class Meta: + model = IssueRelation + fields = [ + "issue_detail", + "relation_type", + "related_issue", + "issue", + "id" + ] + read_only_fields = [ + "workspace", + "project", + ] + +class RelatedIssueSerializer(BaseSerializer): + issue_detail = IssueProjectLiteSerializer(read_only=True, source="issue") + + class Meta: + model = IssueRelation + fields = [ + "issue_detail", + "relation_type", + "related_issue", + "issue", + "id" + ] + read_only_fields = [ + "workspace", + "project", + ] + + +class IssueCycleDetailSerializer(BaseSerializer): + cycle_detail = CycleBaseSerializer(read_only=True, source="cycle") + + class Meta: + model = CycleIssue + fields = "__all__" + read_only_fields = [ + "workspace", + "project", + "created_by", + "updated_by", + "created_at", + "updated_at", + ] + + +class IssueModuleDetailSerializer(BaseSerializer): + module_detail = ModuleBaseSerializer(read_only=True, source="module") + + class Meta: + model = ModuleIssue + fields = "__all__" + read_only_fields = [ + "workspace", + "project", + "created_by", + "updated_by", + "created_at", + "updated_at", + ] + + +class IssueLinkSerializer(BaseSerializer): + created_by_detail = UserLiteSerializer(read_only=True, source="created_by") + + class Meta: + model = IssueLink + fields = "__all__" + read_only_fields = [ + "workspace", + "project", + "created_by", + "updated_by", + "created_at", + "updated_at", + "issue", + ] + + # Validation if url already exists + def create(self, validated_data): + if IssueLink.objects.filter( + url=validated_data.get("url"), issue_id=validated_data.get("issue_id") + ).exists(): + raise serializers.ValidationError( + {"error": "URL already exists for this Issue"} + ) + return IssueLink.objects.create(**validated_data) + + +class IssueAttachmentSerializer(BaseSerializer): + class Meta: + model = IssueAttachment + fields = "__all__" + read_only_fields = [ + "created_by", + "updated_by", + "created_at", + "updated_at", + "workspace", + "project", + "issue", + ] + + +class IssueReactionSerializer(BaseSerializer): + + actor_detail = UserLiteSerializer(read_only=True, source="actor") + + class Meta: + model = IssueReaction + fields = "__all__" + read_only_fields = [ + "workspace", + "project", + "issue", + "actor", + ] + + +class IssueSerializer(BaseSerializer): + project_detail = ProjectLiteSerializer(read_only=True, source="project") + state_detail = StateSerializer(read_only=True, source="state") + parent_detail = IssueStateFlatSerializer(read_only=True, source="parent") + label_details = LabelSerializer(read_only=True, source="labels", many=True) + assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True) + related_issues = IssueRelationSerializer(read_only=True, source="issue_relation", many=True) + issue_relations = RelatedIssueSerializer(read_only=True, source="issue_related", many=True) + issue_cycle = IssueCycleDetailSerializer(read_only=True) + issue_module = IssueModuleDetailSerializer(read_only=True) + issue_link = IssueLinkSerializer(read_only=True, many=True) + issue_attachment = IssueAttachmentSerializer(read_only=True, many=True) + sub_issues_count = serializers.IntegerField(read_only=True) + issue_reactions = IssueReactionSerializer(read_only=True, many=True) + + class Meta: + model = Issue + fields = "__all__" + read_only_fields = [ + "workspace", + "project", + "created_by", + "updated_by", + "created_at", + "updated_at", + ] + + +class IssueFlatSerializer(BaseSerializer): + ## Contain only flat fields + + class Meta: + model = Issue + fields = [ + "id", + "name", + "description", + "description_html", + "priority", + "start_date", + "target_date", + "sequence_id", + "sort_order", + "is_draft", + ] + + +class CommentReactionLiteSerializer(BaseSerializer): + actor_detail = UserLiteSerializer(read_only=True, source="actor") + + class Meta: + model = CommentReaction + fields = [ + "id", + "reaction", + "comment", + "actor_detail", + ] + + +class IssueCommentSerializer(BaseSerializer): + actor_detail = UserLiteSerializer(read_only=True, source="actor") + issue_detail = IssueFlatSerializer(read_only=True, source="issue") + project_detail = ProjectLiteSerializer(read_only=True, source="project") + workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace") + comment_reactions = CommentReactionLiteSerializer(read_only=True, many=True) + is_member = serializers.BooleanField(read_only=True) + + class Meta: + model = IssueComment + fields = "__all__" + read_only_fields = [ + "workspace", + "project", + "issue", + "created_by", + "updated_by", + "created_at", + "updated_at", + ] + + +##TODO: Find a better way to write this serializer +## Find a better approach to save manytomany? +class IssueCreateSerializer(BaseSerializer): + state_detail = StateSerializer(read_only=True, source="state") + created_by_detail = UserLiteSerializer(read_only=True, source="created_by") + project_detail = ProjectLiteSerializer(read_only=True, source="project") + workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace") + + assignees = serializers.ListField( + child=serializers.PrimaryKeyRelatedField(queryset=User.objects.all()), + write_only=True, + required=False, + ) + + labels = serializers.ListField( + child=serializers.PrimaryKeyRelatedField(queryset=Label.objects.all()), + write_only=True, + required=False, + ) + + class Meta: + model = Issue + fields = "__all__" + read_only_fields = [ + "workspace", + "project", + "created_by", + "updated_by", + "created_at", + "updated_at", + ] + + def to_representation(self, instance): + data = super().to_representation(instance) + data['assignees'] = [str(assignee.id) for assignee in instance.assignees.all()] + data['labels'] = [str(label.id) for label in instance.labels.all()] + return data + + 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) + ): + raise serializers.ValidationError("Start date cannot exceed target date") + return data + + def create(self, validated_data): + assignees = validated_data.pop("assignees", None) + labels = validated_data.pop("labels", None) + + project_id = self.context["project_id"] + workspace_id = self.context["workspace_id"] + default_assignee_id = self.context["default_assignee_id"] + + issue = Issue.objects.create(**validated_data, project_id=project_id) + + # Issue Audit Users + created_by_id = issue.created_by_id + updated_by_id = issue.updated_by_id + + if assignees is not None and len(assignees): + IssueAssignee.objects.bulk_create( + [ + IssueAssignee( + assignee=user, + issue=issue, + project_id=project_id, + workspace_id=workspace_id, + created_by_id=created_by_id, + updated_by_id=updated_by_id, + ) + for user in assignees + ], + batch_size=10, + ) + else: + # Then assign it to default assignee + if default_assignee_id is not None: + IssueAssignee.objects.create( + assignee_id=default_assignee_id, + issue=issue, + project_id=project_id, + workspace_id=workspace_id, + created_by_id=created_by_id, + updated_by_id=updated_by_id, + ) + + if labels is not None and len(labels): + IssueLabel.objects.bulk_create( + [ + IssueLabel( + label=label, + issue=issue, + project_id=project_id, + workspace_id=workspace_id, + created_by_id=created_by_id, + updated_by_id=updated_by_id, + ) + for label in labels + ], + batch_size=10, + ) + + return issue + + def update(self, instance, validated_data): + assignees = validated_data.pop("assignees", None) + labels = validated_data.pop("labels", None) + + # Related models + project_id = instance.project_id + workspace_id = instance.workspace_id + created_by_id = instance.created_by_id + updated_by_id = instance.updated_by_id + + if assignees is not None: + IssueAssignee.objects.filter(issue=instance).delete() + IssueAssignee.objects.bulk_create( + [ + IssueAssignee( + assignee=user, + issue=instance, + project_id=project_id, + workspace_id=workspace_id, + created_by_id=created_by_id, + updated_by_id=updated_by_id, + ) + for user in assignees + ], + batch_size=10, + ) + + if labels is not None: + IssueLabel.objects.filter(issue=instance).delete() + IssueLabel.objects.bulk_create( + [ + IssueLabel( + label=label, + issue=instance, + project_id=project_id, + workspace_id=workspace_id, + created_by_id=created_by_id, + updated_by_id=updated_by_id, + ) + for label in labels + ], + batch_size=10, + ) + + # Time updation occues even when other related models are updated + instance.updated_at = timezone.now() + return super().update(instance, validated_data) + + +class IssueReactionSerializer(BaseSerializer): + + actor_detail = UserLiteSerializer(read_only=True, source="actor") + + class Meta: + model = IssueReaction + fields = "__all__" + read_only_fields = [ + "workspace", + "project", + "issue", + "actor", + ] + + +class CommentReactionSerializer(BaseSerializer): + class Meta: + model = CommentReaction + fields = "__all__" + read_only_fields = ["workspace", "project", "comment", "actor"] + + +class IssueVoteSerializer(BaseSerializer): + + actor_detail = UserLiteSerializer(read_only=True, source="actor") + + class Meta: + model = IssueVote + fields = ["issue", "vote", "workspace", "project", "actor", "actor_detail"] + read_only_fields = fields + + +class IssuePublicSerializer(BaseSerializer): + project_detail = ProjectLiteSerializer(read_only=True, source="project") + state_detail = StateLiteSerializer(read_only=True, source="state") + reactions = IssueReactionSerializer(read_only=True, many=True, source="issue_reactions") + votes = IssueVoteSerializer(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", + "reactions", + "votes", + ] + read_only_fields = fields + + +class LabelLiteSerializer(BaseSerializer): + class Meta: + model = Label + fields = [ + "id", + "name", + "color", + ] + + + + diff --git a/apiserver/plane/space/serializer/module.py b/apiserver/plane/space/serializer/module.py new file mode 100644 index 000000000..39ce9ec32 --- /dev/null +++ b/apiserver/plane/space/serializer/module.py @@ -0,0 +1,18 @@ +# Module imports +from .base import BaseSerializer +from plane.db.models import ( + Module, +) + +class ModuleBaseSerializer(BaseSerializer): + class Meta: + model = Module + fields = "__all__" + read_only_fields = [ + "workspace", + "project", + "created_by", + "updated_by", + "created_at", + "updated_at", + ] \ No newline at end of file diff --git a/apiserver/plane/space/serializer/project.py b/apiserver/plane/space/serializer/project.py new file mode 100644 index 000000000..be23e0ce2 --- /dev/null +++ b/apiserver/plane/space/serializer/project.py @@ -0,0 +1,20 @@ +# Module imports +from .base import BaseSerializer +from plane.db.models import ( + Project, +) + + +class ProjectLiteSerializer(BaseSerializer): + class Meta: + model = Project + fields = [ + "id", + "identifier", + "name", + "cover_image", + "icon_prop", + "emoji", + "description", + ] + read_only_fields = fields diff --git a/apiserver/plane/space/serializer/state.py b/apiserver/plane/space/serializer/state.py new file mode 100644 index 000000000..903bcc2f4 --- /dev/null +++ b/apiserver/plane/space/serializer/state.py @@ -0,0 +1,28 @@ +# Module imports +from .base import BaseSerializer +from plane.db.models import ( + State, +) + + +class StateSerializer(BaseSerializer): + + class Meta: + model = State + fields = "__all__" + read_only_fields = [ + "workspace", + "project", + ] + + +class StateLiteSerializer(BaseSerializer): + class Meta: + model = State + fields = [ + "id", + "name", + "color", + "group", + ] + read_only_fields = fields diff --git a/apiserver/plane/space/serializer/user.py b/apiserver/plane/space/serializer/user.py new file mode 100644 index 000000000..e206073f7 --- /dev/null +++ b/apiserver/plane/space/serializer/user.py @@ -0,0 +1,22 @@ +# Module imports +from .base import BaseSerializer +from plane.db.models import ( + User, +) + + +class UserLiteSerializer(BaseSerializer): + class Meta: + model = User + fields = [ + "id", + "first_name", + "last_name", + "avatar", + "is_bot", + "display_name", + ] + read_only_fields = [ + "id", + "is_bot", + ] diff --git a/apiserver/plane/space/serializer/workspace.py b/apiserver/plane/space/serializer/workspace.py new file mode 100644 index 000000000..ecf99079f --- /dev/null +++ b/apiserver/plane/space/serializer/workspace.py @@ -0,0 +1,15 @@ +# Module imports +from .base import BaseSerializer +from plane.db.models import ( + Workspace, +) + +class WorkspaceLiteSerializer(BaseSerializer): + class Meta: + model = Workspace + fields = [ + "name", + "slug", + "id", + ] + read_only_fields = fields \ No newline at end of file diff --git a/apiserver/plane/space/urls/__init__.py b/apiserver/plane/space/urls/__init__.py new file mode 100644 index 000000000..054026b00 --- /dev/null +++ b/apiserver/plane/space/urls/__init__.py @@ -0,0 +1,10 @@ +from .inbox import urlpatterns as inbox_urls +from .issue import urlpatterns as issue_urls +from .project import urlpatterns as project_urls + + +urlpatterns = [ + *inbox_urls, + *issue_urls, + *project_urls, +] diff --git a/apiserver/plane/space/urls/inbox.py b/apiserver/plane/space/urls/inbox.py new file mode 100644 index 000000000..60de040e2 --- /dev/null +++ b/apiserver/plane/space/urls/inbox.py @@ -0,0 +1,49 @@ +from django.urls import path + + +from plane.space.views import ( + InboxIssuePublicViewSet, + IssueVotePublicViewSet, + WorkspaceProjectDeployBoardEndpoint, +) + + +urlpatterns = [ + path( + "workspaces//project-boards//inboxes//inbox-issues/", + InboxIssuePublicViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="inbox-issue", + ), + path( + "workspaces//project-boards//inboxes//inbox-issues//", + InboxIssuePublicViewSet.as_view( + { + "get": "retrieve", + "patch": "partial_update", + "delete": "destroy", + } + ), + name="inbox-issue", + ), + path( + "workspaces//project-boards//issues//votes/", + IssueVotePublicViewSet.as_view( + { + "get": "list", + "post": "create", + "delete": "destroy", + } + ), + name="issue-vote-project-board", + ), + path( + "workspaces//project-boards/", + WorkspaceProjectDeployBoardEndpoint.as_view(), + name="workspace-project-boards", + ), +] diff --git a/apiserver/plane/space/urls/issue.py b/apiserver/plane/space/urls/issue.py new file mode 100644 index 000000000..099eace5d --- /dev/null +++ b/apiserver/plane/space/urls/issue.py @@ -0,0 +1,76 @@ +from django.urls import path + + +from plane.space.views import ( + IssueRetrievePublicEndpoint, + IssueCommentPublicViewSet, + IssueReactionPublicViewSet, + CommentReactionPublicViewSet, +) + +urlpatterns = [ + path( + "workspaces//project-boards//issues//", + IssueRetrievePublicEndpoint.as_view(), + name="workspace-project-boards", + ), + path( + "workspaces//project-boards//issues//comments/", + IssueCommentPublicViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="issue-comments-project-board", + ), + path( + "workspaces//project-boards//issues//comments//", + IssueCommentPublicViewSet.as_view( + { + "get": "retrieve", + "patch": "partial_update", + "delete": "destroy", + } + ), + name="issue-comments-project-board", + ), + path( + "workspaces//project-boards//issues//reactions/", + IssueReactionPublicViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="issue-reactions-project-board", + ), + path( + "workspaces//project-boards//issues//reactions//", + IssueReactionPublicViewSet.as_view( + { + "delete": "destroy", + } + ), + name="issue-reactions-project-board", + ), + path( + "workspaces//project-boards//comments//reactions/", + CommentReactionPublicViewSet.as_view( + { + "get": "list", + "post": "create", + } + ), + name="comment-reactions-project-board", + ), + path( + "workspaces//project-boards//comments//reactions//", + CommentReactionPublicViewSet.as_view( + { + "delete": "destroy", + } + ), + name="comment-reactions-project-board", + ), +] diff --git a/apiserver/plane/space/urls/project.py b/apiserver/plane/space/urls/project.py new file mode 100644 index 000000000..dc97b43a7 --- /dev/null +++ b/apiserver/plane/space/urls/project.py @@ -0,0 +1,20 @@ +from django.urls import path + + +from plane.space.views import ( + ProjectDeployBoardPublicSettingsEndpoint, + ProjectIssuesPublicEndpoint, +) + +urlpatterns = [ + path( + "workspaces//project-boards//settings/", + ProjectDeployBoardPublicSettingsEndpoint.as_view(), + name="project-deploy-board-settings", + ), + path( + "workspaces//project-boards//issues/", + ProjectIssuesPublicEndpoint.as_view(), + name="project-deploy-board", + ), +] diff --git a/apiserver/plane/space/views/__init__.py b/apiserver/plane/space/views/__init__.py new file mode 100644 index 000000000..5130e04d5 --- /dev/null +++ b/apiserver/plane/space/views/__init__.py @@ -0,0 +1,15 @@ +from .project import ( + ProjectDeployBoardPublicSettingsEndpoint, + WorkspaceProjectDeployBoardEndpoint, +) + +from .issue import ( + IssueCommentPublicViewSet, + IssueReactionPublicViewSet, + CommentReactionPublicViewSet, + IssueVotePublicViewSet, + IssueRetrievePublicEndpoint, + ProjectIssuesPublicEndpoint, +) + +from .inbox import InboxIssuePublicViewSet diff --git a/apiserver/plane/space/views/base.py b/apiserver/plane/space/views/base.py new file mode 100644 index 000000000..b1d749a09 --- /dev/null +++ b/apiserver/plane/space/views/base.py @@ -0,0 +1,212 @@ +# Python imports +import zoneinfo + +# Django imports +from django.urls import resolve +from django.conf import settings +from django.utils import timezone +from django.db import IntegrityError +from django.core.exceptions import ObjectDoesNotExist, ValidationError + +# Third part imports +from rest_framework import status +from rest_framework import status +from rest_framework.viewsets import ModelViewSet +from rest_framework.response import Response +from rest_framework.exceptions import APIException +from rest_framework.views import APIView +from rest_framework.filters import SearchFilter +from rest_framework.permissions import IsAuthenticated +from sentry_sdk import capture_exception +from django_filters.rest_framework import DjangoFilterBackend + +# Module imports +from plane.utils.paginator import BasePaginator + + +class TimezoneMixin: + """ + This enables timezone conversion according + to the user set timezone + """ + + def initial(self, request, *args, **kwargs): + super().initial(request, *args, **kwargs) + if request.user.is_authenticated: + timezone.activate(zoneinfo.ZoneInfo(request.user.user_timezone)) + else: + timezone.deactivate() + + +class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator): + model = None + + permission_classes = [ + IsAuthenticated, + ] + + filter_backends = ( + DjangoFilterBackend, + SearchFilter, + ) + + filterset_fields = [] + + search_fields = [] + + def get_queryset(self): + try: + return self.model.objects.all() + except Exception as e: + capture_exception(e) + raise APIException("Please check the view", status.HTTP_400_BAD_REQUEST) + + def handle_exception(self, exc): + """ + Handle any exception that occurs, by returning an appropriate response, + or re-raising the error. + """ + try: + response = super().handle_exception(exc) + return response + except Exception as e: + if isinstance(e, IntegrityError): + return Response( + {"error": "The payload is not valid"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + if isinstance(e, ValidationError): + return Response( + {"error": "Please provide valid detail"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + if isinstance(e, ObjectDoesNotExist): + model_name = str(exc).split(" matching query does not exist.")[0] + return Response( + {"error": f"{model_name} does not exist."}, + status=status.HTTP_404_NOT_FOUND, + ) + + if isinstance(e, KeyError): + capture_exception(e) + return Response( + {"error": f"key {e} does not exist"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + print(e) if settings.DEBUG else print("Server Error") + capture_exception(e) + return Response({"error": "Something went wrong please try again later"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + + def dispatch(self, request, *args, **kwargs): + try: + response = super().dispatch(request, *args, **kwargs) + + if settings.DEBUG: + from django.db import connection + + print( + f"{request.method} - {request.get_full_path()} of Queries: {len(connection.queries)}" + ) + + return response + except Exception as exc: + response = self.handle_exception(exc) + return exc + + @property + def workspace_slug(self): + return self.kwargs.get("slug", None) + + @property + def project_id(self): + project_id = self.kwargs.get("project_id", None) + if project_id: + return project_id + + if resolve(self.request.path_info).url_name == "project": + return self.kwargs.get("pk", None) + + +class BaseAPIView(TimezoneMixin, APIView, BasePaginator): + permission_classes = [ + IsAuthenticated, + ] + + filter_backends = ( + DjangoFilterBackend, + SearchFilter, + ) + + filterset_fields = [] + + search_fields = [] + + def filter_queryset(self, queryset): + for backend in list(self.filter_backends): + queryset = backend().filter_queryset(self.request, queryset, self) + return queryset + + def handle_exception(self, exc): + """ + Handle any exception that occurs, by returning an appropriate response, + or re-raising the error. + """ + try: + response = super().handle_exception(exc) + return response + except Exception as e: + if isinstance(e, IntegrityError): + return Response( + {"error": "The payload is not valid"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + if isinstance(e, ValidationError): + return Response( + {"error": "Please provide valid detail"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + if isinstance(e, ObjectDoesNotExist): + model_name = str(exc).split(" matching query does not exist.")[0] + return Response( + {"error": f"{model_name} does not exist."}, + status=status.HTTP_404_NOT_FOUND, + ) + + if isinstance(e, KeyError): + return Response({"error": f"key {e} does not exist"}, status=status.HTTP_400_BAD_REQUEST) + + if settings.DEBUG: + print(e) + capture_exception(e) + return Response({"error": "Something went wrong please try again later"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + + def dispatch(self, request, *args, **kwargs): + try: + response = super().dispatch(request, *args, **kwargs) + + if settings.DEBUG: + from django.db import connection + + print( + f"{request.method} - {request.get_full_path()} of Queries: {len(connection.queries)}" + ) + return response + + except Exception as exc: + response = self.handle_exception(exc) + return exc + + @property + def workspace_slug(self): + return self.kwargs.get("slug", None) + + @property + def project_id(self): + return self.kwargs.get("project_id", None) diff --git a/apiserver/plane/space/views/inbox.py b/apiserver/plane/space/views/inbox.py new file mode 100644 index 000000000..53960f672 --- /dev/null +++ b/apiserver/plane/space/views/inbox.py @@ -0,0 +1,282 @@ +# Python imports +import json + +# Django import +from django.utils import timezone +from django.db.models import Q, OuterRef, Func, F, Prefetch +from django.core.serializers.json import DjangoJSONEncoder + +# Third party imports +from rest_framework import status +from rest_framework.response import Response + +# Module imports +from .base import BaseViewSet +from plane.db.models import ( + InboxIssue, + Issue, + State, + IssueLink, + IssueAttachment, + ProjectDeployBoard, +) +from plane.app.serializers import ( + IssueSerializer, + InboxIssueSerializer, + IssueCreateSerializer, + IssueStateInboxSerializer, +) +from plane.utils.issue_filters import issue_filters +from plane.bgtasks.issue_activites_task import issue_activity + + +class InboxIssuePublicViewSet(BaseViewSet): + serializer_class = InboxIssueSerializer + model = InboxIssue + + filterset_fields = [ + "status", + ] + + def get_queryset(self): + project_deploy_board = ProjectDeployBoard.objects.get( + workspace__slug=self.kwargs.get("slug"), + project_id=self.kwargs.get("project_id"), + ) + if project_deploy_board is not None: + return self.filter_queryset( + super() + .get_queryset() + .filter( + Q(snoozed_till__gte=timezone.now()) | Q(snoozed_till__isnull=True), + project_id=self.kwargs.get("project_id"), + workspace__slug=self.kwargs.get("slug"), + inbox_id=self.kwargs.get("inbox_id"), + ) + .select_related("issue", "workspace", "project") + ) + return InboxIssue.objects.none() + + def list(self, request, slug, project_id, inbox_id): + project_deploy_board = ProjectDeployBoard.objects.get( + workspace__slug=slug, project_id=project_id + ) + if project_deploy_board.inbox is None: + return Response( + {"error": "Inbox is not enabled for this Project Board"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + filters = issue_filters(request.query_params, "GET") + issues = ( + Issue.objects.filter( + issue_inbox__inbox_id=inbox_id, + workspace__slug=slug, + project_id=project_id, + ) + .filter(**filters) + .annotate(bridge_id=F("issue_inbox__id")) + .select_related("workspace", "project", "state", "parent") + .prefetch_related("assignees", "labels") + .order_by("issue_inbox__snoozed_till", "issue_inbox__status") + .annotate( + sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id")) + .order_by() + .annotate(count=Func(F("id"), function="Count")) + .values("count") + ) + .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") + ) + .prefetch_related( + Prefetch( + "issue_inbox", + queryset=InboxIssue.objects.only( + "status", "duplicate_to", "snoozed_till", "source" + ), + ) + ) + ) + issues_data = IssueStateInboxSerializer(issues, many=True).data + return Response( + issues_data, + status=status.HTTP_200_OK, + ) + + def create(self, request, slug, project_id, inbox_id): + project_deploy_board = ProjectDeployBoard.objects.get( + workspace__slug=slug, project_id=project_id + ) + if project_deploy_board.inbox is None: + return Response( + {"error": "Inbox is not enabled for this Project Board"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + if not request.data.get("issue", {}).get("name", False): + return Response( + {"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST + ) + + # Check for valid priority + if not request.data.get("issue", {}).get("priority", "none") in [ + "low", + "medium", + "high", + "urgent", + "none", + ]: + return Response( + {"error": "Invalid priority"}, status=status.HTTP_400_BAD_REQUEST + ) + + # Create or get state + state, _ = State.objects.get_or_create( + name="Triage", + group="backlog", + description="Default state for managing all Inbox Issues", + project_id=project_id, + color="#ff7700", + ) + + # create an issue + issue = Issue.objects.create( + name=request.data.get("issue", {}).get("name"), + description=request.data.get("issue", {}).get("description", {}), + description_html=request.data.get("issue", {}).get( + "description_html", "

" + ), + priority=request.data.get("issue", {}).get("priority", "low"), + project_id=project_id, + state=state, + ) + + # Create an Issue Activity + issue_activity.delay( + type="issue.activity.created", + requested_data=json.dumps(request.data, cls=DjangoJSONEncoder), + actor_id=str(request.user.id), + issue_id=str(issue.id), + project_id=str(project_id), + current_instance=None, + epoch=int(timezone.now().timestamp()), + ) + # create an inbox issue + InboxIssue.objects.create( + inbox_id=inbox_id, + project_id=project_id, + issue=issue, + source=request.data.get("source", "in-app"), + ) + + serializer = IssueStateInboxSerializer(issue) + return Response(serializer.data, status=status.HTTP_200_OK) + + def partial_update(self, request, slug, project_id, inbox_id, pk): + project_deploy_board = ProjectDeployBoard.objects.get( + workspace__slug=slug, project_id=project_id + ) + if project_deploy_board.inbox is None: + return Response( + {"error": "Inbox is not enabled for this Project Board"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + inbox_issue = InboxIssue.objects.get( + pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id + ) + # Get the project member + if str(inbox_issue.created_by_id) != str(request.user.id): + return Response( + {"error": "You cannot edit inbox issues"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + # Get issue data + issue_data = request.data.pop("issue", False) + + issue = Issue.objects.get( + pk=inbox_issue.issue_id, workspace__slug=slug, project_id=project_id + ) + # viewers and guests since only viewers and guests + issue_data = { + "name": issue_data.get("name", issue.name), + "description_html": issue_data.get( + "description_html", issue.description_html + ), + "description": issue_data.get("description", issue.description), + } + + issue_serializer = IssueCreateSerializer(issue, data=issue_data, partial=True) + + if issue_serializer.is_valid(): + current_instance = issue + # Log all the updates + requested_data = json.dumps(issue_data, cls=DjangoJSONEncoder) + if issue is not None: + issue_activity.delay( + type="issue.activity.updated", + requested_data=requested_data, + actor_id=str(request.user.id), + issue_id=str(issue.id), + project_id=str(project_id), + current_instance=json.dumps( + IssueSerializer(current_instance).data, + cls=DjangoJSONEncoder, + ), + epoch=int(timezone.now().timestamp()), + ) + issue_serializer.save() + return Response(issue_serializer.data, status=status.HTTP_200_OK) + return Response(issue_serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def retrieve(self, request, slug, project_id, inbox_id, pk): + project_deploy_board = ProjectDeployBoard.objects.get( + workspace__slug=slug, project_id=project_id + ) + if project_deploy_board.inbox is None: + return Response( + {"error": "Inbox is not enabled for this Project Board"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + inbox_issue = InboxIssue.objects.get( + pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id + ) + issue = Issue.objects.get( + pk=inbox_issue.issue_id, workspace__slug=slug, project_id=project_id + ) + serializer = IssueStateInboxSerializer(issue) + return Response(serializer.data, status=status.HTTP_200_OK) + + def destroy(self, request, slug, project_id, inbox_id, pk): + project_deploy_board = ProjectDeployBoard.objects.get( + workspace__slug=slug, project_id=project_id + ) + if project_deploy_board.inbox is None: + return Response( + {"error": "Inbox is not enabled for this Project Board"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + inbox_issue = InboxIssue.objects.get( + pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id + ) + + if str(inbox_issue.created_by_id) != str(request.user.id): + return Response( + {"error": "You cannot delete inbox issue"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + inbox_issue.delete() + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/space/views/issue.py b/apiserver/plane/space/views/issue.py new file mode 100644 index 000000000..faab8834d --- /dev/null +++ b/apiserver/plane/space/views/issue.py @@ -0,0 +1,656 @@ +# Python imports +import json + +# Django imports +from django.utils import timezone +from django.db.models import ( + Prefetch, + OuterRef, + Func, + F, + Q, + Count, + Case, + Value, + CharField, + When, + Exists, + Max, + IntegerField, +) +from django.core.serializers.json import DjangoJSONEncoder + +# Third Party imports +from rest_framework.response import Response +from rest_framework import status +from rest_framework.permissions import AllowAny, IsAuthenticated + +# Module imports +from .base import BaseViewSet, BaseAPIView +from plane.app.serializers import ( + IssueCommentSerializer, + IssueReactionSerializer, + CommentReactionSerializer, + IssueVoteSerializer, + IssuePublicSerializer, +) + +from plane.db.models import ( + Issue, + IssueComment, + Label, + IssueLink, + IssueAttachment, + State, + ProjectMember, + IssueReaction, + CommentReaction, + ProjectDeployBoard, + IssueVote, + ProjectPublicMember, +) +from plane.bgtasks.issue_activites_task import issue_activity +from plane.utils.grouper import group_results +from plane.utils.issue_filters import issue_filters + + +class IssueCommentPublicViewSet(BaseViewSet): + serializer_class = IssueCommentSerializer + model = IssueComment + + filterset_fields = [ + "issue__id", + "workspace__id", + ] + + def get_permissions(self): + if self.action in ["list", "retrieve"]: + self.permission_classes = [ + AllowAny, + ] + else: + self.permission_classes = [ + IsAuthenticated, + ] + + return super(IssueCommentPublicViewSet, self).get_permissions() + + def get_queryset(self): + try: + project_deploy_board = ProjectDeployBoard.objects.get( + workspace__slug=self.kwargs.get("slug"), + project_id=self.kwargs.get("project_id"), + ) + if project_deploy_board.comments: + return self.filter_queryset( + super() + .get_queryset() + .filter(workspace__slug=self.kwargs.get("slug")) + .filter(issue_id=self.kwargs.get("issue_id")) + .filter(access="EXTERNAL") + .select_related("project") + .select_related("workspace") + .select_related("issue") + .annotate( + is_member=Exists( + ProjectMember.objects.filter( + workspace__slug=self.kwargs.get("slug"), + project_id=self.kwargs.get("project_id"), + member_id=self.request.user.id, + is_active=True, + ) + ) + ) + .distinct() + ).order_by("created_at") + return IssueComment.objects.none() + except ProjectDeployBoard.DoesNotExist: + return IssueComment.objects.none() + + def create(self, request, slug, project_id, issue_id): + project_deploy_board = ProjectDeployBoard.objects.get( + workspace__slug=slug, project_id=project_id + ) + + if not project_deploy_board.comments: + return Response( + {"error": "Comments are not enabled for this project"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + serializer = IssueCommentSerializer(data=request.data) + if serializer.is_valid(): + serializer.save( + project_id=project_id, + issue_id=issue_id, + actor=request.user, + access="EXTERNAL", + ) + issue_activity.delay( + type="comment.activity.created", + requested_data=json.dumps(serializer.data, cls=DjangoJSONEncoder), + actor_id=str(request.user.id), + issue_id=str(issue_id), + project_id=str(project_id), + current_instance=None, + epoch=int(timezone.now().timestamp()), + ) + if not ProjectMember.objects.filter( + project_id=project_id, + member=request.user, + is_active=True, + ).exists(): + # Add the user for workspace tracking + _ = ProjectPublicMember.objects.get_or_create( + project_id=project_id, + member=request.user, + ) + + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def partial_update(self, request, slug, project_id, issue_id, pk): + project_deploy_board = ProjectDeployBoard.objects.get( + workspace__slug=slug, project_id=project_id + ) + + if not project_deploy_board.comments: + return Response( + {"error": "Comments are not enabled for this project"}, + status=status.HTTP_400_BAD_REQUEST, + ) + comment = IssueComment.objects.get( + workspace__slug=slug, pk=pk, actor=request.user + ) + serializer = IssueCommentSerializer(comment, data=request.data, partial=True) + if serializer.is_valid(): + serializer.save() + issue_activity.delay( + type="comment.activity.updated", + requested_data=json.dumps(request.data, cls=DjangoJSONEncoder), + actor_id=str(request.user.id), + issue_id=str(issue_id), + project_id=str(project_id), + current_instance=json.dumps( + IssueCommentSerializer(comment).data, + cls=DjangoJSONEncoder, + ), + epoch=int(timezone.now().timestamp()), + ) + return Response(serializer.data, status=status.HTTP_200_OK) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def destroy(self, request, slug, project_id, issue_id, pk): + project_deploy_board = ProjectDeployBoard.objects.get( + workspace__slug=slug, project_id=project_id + ) + + if not project_deploy_board.comments: + return Response( + {"error": "Comments are not enabled for this project"}, + status=status.HTTP_400_BAD_REQUEST, + ) + comment = IssueComment.objects.get( + workspace__slug=slug, pk=pk, project_id=project_id, actor=request.user + ) + issue_activity.delay( + type="comment.activity.deleted", + requested_data=json.dumps({"comment_id": str(pk)}), + actor_id=str(request.user.id), + issue_id=str(issue_id), + project_id=str(project_id), + current_instance=json.dumps( + IssueCommentSerializer(comment).data, + cls=DjangoJSONEncoder, + ), + epoch=int(timezone.now().timestamp()), + ) + comment.delete() + return Response(status=status.HTTP_204_NO_CONTENT) + + +class IssueReactionPublicViewSet(BaseViewSet): + serializer_class = IssueReactionSerializer + model = IssueReaction + + def get_queryset(self): + try: + project_deploy_board = ProjectDeployBoard.objects.get( + workspace__slug=self.kwargs.get("slug"), + project_id=self.kwargs.get("project_id"), + ) + if project_deploy_board.reactions: + return ( + super() + .get_queryset() + .filter(workspace__slug=self.kwargs.get("slug")) + .filter(project_id=self.kwargs.get("project_id")) + .filter(issue_id=self.kwargs.get("issue_id")) + .order_by("-created_at") + .distinct() + ) + return IssueReaction.objects.none() + except ProjectDeployBoard.DoesNotExist: + return IssueReaction.objects.none() + + def create(self, request, slug, project_id, issue_id): + project_deploy_board = ProjectDeployBoard.objects.get( + workspace__slug=slug, project_id=project_id + ) + + if not project_deploy_board.reactions: + return Response( + {"error": "Reactions are not enabled for this project board"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + serializer = IssueReactionSerializer(data=request.data) + if serializer.is_valid(): + serializer.save( + project_id=project_id, issue_id=issue_id, actor=request.user + ) + if not ProjectMember.objects.filter( + project_id=project_id, + member=request.user, + is_active=True, + ).exists(): + # Add the user for workspace tracking + _ = ProjectPublicMember.objects.get_or_create( + project_id=project_id, + member=request.user, + ) + issue_activity.delay( + type="issue_reaction.activity.created", + requested_data=json.dumps(self.request.data, cls=DjangoJSONEncoder), + actor_id=str(self.request.user.id), + issue_id=str(self.kwargs.get("issue_id", None)), + project_id=str(self.kwargs.get("project_id", None)), + current_instance=None, + epoch=int(timezone.now().timestamp()), + ) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def destroy(self, request, slug, project_id, issue_id, reaction_code): + project_deploy_board = ProjectDeployBoard.objects.get( + workspace__slug=slug, project_id=project_id + ) + + if not project_deploy_board.reactions: + return Response( + {"error": "Reactions are not enabled for this project board"}, + status=status.HTTP_400_BAD_REQUEST, + ) + issue_reaction = IssueReaction.objects.get( + workspace__slug=slug, + issue_id=issue_id, + reaction=reaction_code, + actor=request.user, + ) + issue_activity.delay( + type="issue_reaction.activity.deleted", + requested_data=None, + actor_id=str(self.request.user.id), + issue_id=str(self.kwargs.get("issue_id", None)), + project_id=str(self.kwargs.get("project_id", None)), + current_instance=json.dumps( + { + "reaction": str(reaction_code), + "identifier": str(issue_reaction.id), + } + ), + epoch=int(timezone.now().timestamp()), + ) + issue_reaction.delete() + return Response(status=status.HTTP_204_NO_CONTENT) + + +class CommentReactionPublicViewSet(BaseViewSet): + serializer_class = CommentReactionSerializer + model = CommentReaction + + def get_queryset(self): + try: + project_deploy_board = ProjectDeployBoard.objects.get( + workspace__slug=self.kwargs.get("slug"), + project_id=self.kwargs.get("project_id"), + ) + if project_deploy_board.reactions: + return ( + super() + .get_queryset() + .filter(workspace__slug=self.kwargs.get("slug")) + .filter(project_id=self.kwargs.get("project_id")) + .filter(comment_id=self.kwargs.get("comment_id")) + .order_by("-created_at") + .distinct() + ) + return CommentReaction.objects.none() + except ProjectDeployBoard.DoesNotExist: + return CommentReaction.objects.none() + + def create(self, request, slug, project_id, comment_id): + project_deploy_board = ProjectDeployBoard.objects.get( + workspace__slug=slug, project_id=project_id + ) + + if not project_deploy_board.reactions: + return Response( + {"error": "Reactions are not enabled for this board"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + serializer = CommentReactionSerializer(data=request.data) + if serializer.is_valid(): + serializer.save( + project_id=project_id, comment_id=comment_id, actor=request.user + ) + if not ProjectMember.objects.filter( + project_id=project_id, + member=request.user, + is_active=True, + ).exists(): + # Add the user for workspace tracking + _ = ProjectPublicMember.objects.get_or_create( + project_id=project_id, + member=request.user, + ) + issue_activity.delay( + type="comment_reaction.activity.created", + requested_data=json.dumps(self.request.data, cls=DjangoJSONEncoder), + actor_id=str(self.request.user.id), + issue_id=None, + project_id=str(self.kwargs.get("project_id", None)), + current_instance=None, + epoch=int(timezone.now().timestamp()), + ) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def destroy(self, request, slug, project_id, comment_id, reaction_code): + project_deploy_board = ProjectDeployBoard.objects.get( + workspace__slug=slug, project_id=project_id + ) + if not project_deploy_board.reactions: + return Response( + {"error": "Reactions are not enabled for this board"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + comment_reaction = CommentReaction.objects.get( + project_id=project_id, + workspace__slug=slug, + comment_id=comment_id, + reaction=reaction_code, + actor=request.user, + ) + issue_activity.delay( + type="comment_reaction.activity.deleted", + requested_data=None, + actor_id=str(self.request.user.id), + issue_id=None, + project_id=str(self.kwargs.get("project_id", None)), + current_instance=json.dumps( + { + "reaction": str(reaction_code), + "identifier": str(comment_reaction.id), + "comment_id": str(comment_id), + } + ), + epoch=int(timezone.now().timestamp()), + ) + comment_reaction.delete() + return Response(status=status.HTTP_204_NO_CONTENT) + + +class IssueVotePublicViewSet(BaseViewSet): + model = IssueVote + serializer_class = IssueVoteSerializer + + def get_queryset(self): + try: + project_deploy_board = ProjectDeployBoard.objects.get( + workspace__slug=self.kwargs.get("slug"), + project_id=self.kwargs.get("project_id"), + ) + if project_deploy_board.votes: + return ( + super() + .get_queryset() + .filter(issue_id=self.kwargs.get("issue_id")) + .filter(workspace__slug=self.kwargs.get("slug")) + .filter(project_id=self.kwargs.get("project_id")) + ) + return IssueVote.objects.none() + except ProjectDeployBoard.DoesNotExist: + return IssueVote.objects.none() + + def create(self, request, slug, project_id, issue_id): + issue_vote, _ = IssueVote.objects.get_or_create( + actor_id=request.user.id, + project_id=project_id, + issue_id=issue_id, + ) + # Add the user for workspace tracking + if not ProjectMember.objects.filter( + project_id=project_id, + member=request.user, + is_active=True, + ).exists(): + _ = ProjectPublicMember.objects.get_or_create( + project_id=project_id, + member=request.user, + ) + issue_vote.vote = request.data.get("vote", 1) + issue_vote.save() + issue_activity.delay( + type="issue_vote.activity.created", + requested_data=json.dumps(self.request.data, cls=DjangoJSONEncoder), + actor_id=str(self.request.user.id), + issue_id=str(self.kwargs.get("issue_id", None)), + project_id=str(self.kwargs.get("project_id", None)), + current_instance=None, + epoch=int(timezone.now().timestamp()), + ) + serializer = IssueVoteSerializer(issue_vote) + return Response(serializer.data, status=status.HTTP_201_CREATED) + + def destroy(self, request, slug, project_id, issue_id): + issue_vote = IssueVote.objects.get( + workspace__slug=slug, + project_id=project_id, + issue_id=issue_id, + actor_id=request.user.id, + ) + issue_activity.delay( + type="issue_vote.activity.deleted", + requested_data=None, + actor_id=str(self.request.user.id), + issue_id=str(self.kwargs.get("issue_id", None)), + project_id=str(self.kwargs.get("project_id", None)), + current_instance=json.dumps( + { + "vote": str(issue_vote.vote), + "identifier": str(issue_vote.id), + } + ), + epoch=int(timezone.now().timestamp()), + ) + issue_vote.delete() + return Response(status=status.HTTP_204_NO_CONTENT) + + +class IssueRetrievePublicEndpoint(BaseAPIView): + permission_classes = [ + AllowAny, + ] + + def get(self, request, slug, project_id, issue_id): + issue = Issue.objects.get( + workspace__slug=slug, project_id=project_id, pk=issue_id + ) + serializer = IssuePublicSerializer(issue) + return Response(serializer.data, status=status.HTTP_200_OK) + + +class ProjectIssuesPublicEndpoint(BaseAPIView): + permission_classes = [ + AllowAny, + ] + + def get(self, request, slug, project_id): + 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"), + ) + ) + .prefetch_related( + Prefetch( + "votes", + queryset=IssueVote.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 + + state_group_order = [ + "backlog", + "unstarted", + "started", + "completed", + "cancelled", + ] + + states = ( + State.objects.filter( + ~Q(name="Triage"), + workspace__slug=slug, + project_id=project_id, + ) + .annotate( + custom_order=Case( + *[ + When(group=value, then=Value(index)) + for index, value in enumerate(state_group_order) + ], + default=Value(len(state_group_order)), + output_field=IntegerField(), + ), + ) + .values("name", "group", "color", "id") + .order_by("custom_order", "sequence") + ) + + 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, + ) \ No newline at end of file diff --git a/apiserver/plane/space/views/project.py b/apiserver/plane/space/views/project.py new file mode 100644 index 000000000..8cd3f55c5 --- /dev/null +++ b/apiserver/plane/space/views/project.py @@ -0,0 +1,61 @@ +# Django imports +from django.db.models import ( + Exists, + OuterRef, +) + +# Third Party imports +from rest_framework.response import Response +from rest_framework import status +from rest_framework.permissions import AllowAny + +# Module imports +from .base import BaseAPIView +from plane.app.serializers import ProjectDeployBoardSerializer +from plane.app.permissions import ProjectMemberPermission +from plane.db.models import ( + Project, + ProjectDeployBoard, +) + + +class ProjectDeployBoardPublicSettingsEndpoint(BaseAPIView): + permission_classes = [ + AllowAny, + ] + + def get(self, request, slug, project_id): + project_deploy_board = ProjectDeployBoard.objects.get( + workspace__slug=slug, project_id=project_id + ) + serializer = ProjectDeployBoardSerializer(project_deploy_board) + return Response(serializer.data, status=status.HTTP_200_OK) + + +class WorkspaceProjectDeployBoardEndpoint(BaseAPIView): + permission_classes = [ + AllowAny, + ] + + def get(self, request, slug): + projects = ( + Project.objects.filter(workspace__slug=slug) + .annotate( + is_public=Exists( + ProjectDeployBoard.objects.filter( + workspace__slug=slug, project_id=OuterRef("pk") + ) + ) + ) + .filter(is_public=True) + ).values( + "id", + "identifier", + "name", + "description", + "emoji", + "icon_prop", + "cover_image", + ) + + return Response(projects, status=status.HTTP_200_OK) diff --git a/apiserver/plane/tests/api/base.py b/apiserver/plane/tests/api/base.py index fec51303a..e3209a281 100644 --- a/apiserver/plane/tests/api/base.py +++ b/apiserver/plane/tests/api/base.py @@ -3,7 +3,7 @@ from rest_framework.test import APITestCase, APIClient # Module imports from plane.db.models import User -from plane.api.views.authentication import get_tokens_for_user +from plane.app.views.authentication import get_tokens_for_user class BaseAPITest(APITestCase): diff --git a/apiserver/plane/urls.py b/apiserver/plane/urls.py index 66f6714fb..1b6f95bba 100644 --- a/apiserver/plane/urls.py +++ b/apiserver/plane/urls.py @@ -10,7 +10,8 @@ from django.conf import settings urlpatterns = [ path("", TemplateView.as_view(template_name="index.html")), - path("api/", include("plane.api.urls")), + path("api/", include("plane.app.urls")), + path("api/public/", include("plane.space.urls")), path("api/licenses/", include("plane.license.urls")), path("api/v1/", include("plane.proxy.urls")), path("", include("plane.web.urls")),