Compare commits

..

59 Commits

Author SHA1 Message Date
pablohashescobar 1bef55ca88 dev: remove append / setting in common 2023-11-18 19:05:59 +05:30
pablohashescobar c6d6c29e51 dev: update docker environment variable 2023-11-18 18:53:12 +05:30
pablohashescobar 0bb13148b7 dev: fix access token variable error 2023-11-17 12:27:44 +05:30
pablohashescobar 6531e420fc dev: environment configuration for backend 2023-11-17 12:14:39 +05:30
pablohashescobar 93aeb3795d Merge branch 'develop' of github.com:makeplane/plane into feat/self_hosted_instance 2023-11-17 11:52:18 +05:30
pablohashescobar 5afb71ada1 dev: removed DJANGO_SETTINGS_MODULE from environment files and deleted auto bucket create script 2023-11-16 18:33:58 +05:30
pablohashescobar a65da91969 dev: create instance activation route if the instance is not activated during startup 2023-11-16 17:52:55 +05:30
pablohashescobar 63d5951e36 Merge branch 'develop' of github.com:makeplane/plane into feat/self_hosted_instance 2023-11-16 16:55:58 +05:30
pablohashescobar cc158ae6da dev: fix email sending for ssl 2023-11-16 16:49:12 +05:30
pablohashescobar 3f2b890a48 dev: check magic link login 2023-11-16 16:40:16 +05:30
pablohashescobar 936535fef2 dev: instance admin unique constraints 2023-11-16 16:27:27 +05:30
pablohashescobar 29798fa08a Merge branch 'dev/settings_configuration' of github.com:makeplane/plane into feat/self_hosted_instance 2023-11-16 15:07:42 +05:30
pablohashescobar af5534ffe3 Merge branch 'feat/self_hosted_instance' of github.com:makeplane/plane into feat/self_hosted_instance 2023-11-16 13:42:36 +05:30
pablohashescobar 46e733a2c5 dev: admin api to check if the current user is admin 2023-11-16 13:42:03 +05:30
sriram veeraghanta c4d5ccdae4 Merge branch 'feat/self_hosted_instance' of github.com:makeplane/plane into feat/self_hosted_instance 2023-11-16 12:35:23 +05:30
sriram veeraghanta 78d841d67e dev: instance admin routes and initial set of screens 2023-11-16 12:34:41 +05:30
sriram veeraghanta 95c35cde4a Merge branch 'develop' of github.com:makeplane/plane into feat/self_hosted_instance 2023-11-16 12:33:10 +05:30
pablohashescobar 6045e659bc Merge branch 'feat/self_hosted_instance' of github.com:makeplane/plane into feat/self_hosted_instance 2023-11-16 06:46:48 +00:00
pablohashescobar ca79739ead Merge branch 'develop' of github.com:makeplane/plane into feat/self_hosted_instance 2023-11-16 06:45:57 +00:00
pablohashescobar 44a3097ed1 dev: default bucket creation script 2023-11-16 12:13:45 +05:30
pablohashescobar 55869ee994 dev: update script to use manage command 2023-11-15 20:22:14 +05:30
pablohashescobar 6102de37de dev: check email validity 2023-11-15 20:19:55 +05:30
pablohashescobar deefdac8b4 dev: migrate instance registration and configuration to manage commands 2023-11-15 20:07:33 +05:30
pablohashescobar 1e9f4dd938 Merge branch 'feat/self_hosted_instance' of github.com:makeplane/plane into feat/self_hosted_instance 2023-11-15 19:25:04 +05:30
pablohashescobar d9a06a5fe3 Merge branch 'develop' of github.com:makeplane/plane into feat/self_hosted_instance 2023-11-15 19:24:39 +05:30
pablohashescobar 89a5088f31 dev: update cors setup and local settings 2023-11-15 19:22:56 +05:30
pablohashescobar 9789757d2d dev: fix current site domain registration 2023-11-15 18:18:00 +05:30
pablohashescobar 2aee627616 dev: remove deprecated variables 2023-11-15 17:54:33 +05:30
sriram veeraghanta 5b2964dc48 fix: updating env config in case of db values are missing 2023-11-15 16:54:56 +05:30
pablohashescobar 095f64793f dev: fix email senders 2023-11-14 20:31:29 +05:30
pablohashescobar ccfd1c703e dev: fix instance permissions and serializer 2023-11-14 20:30:54 +05:30
pablohashescobar d05b63fc51 dev: instance configuration values 2023-11-14 14:14:11 +00:00
pablohashescobar fea812f1b1 dev: instance owner fix 2023-11-14 11:03:09 +00:00
pablohashescobar 46f2663854 dev: enable instance configuration and instance admin roles 2023-11-14 15:56:48 +05:30
pablohashescobar 1eac8ead86 dev: instance admin 2023-11-14 09:08:24 +00:00
pablohashescobar aca0f5eadb Merge branch 'develop' of github.com:makeplane/plane into feat/self_hosted_instance 2023-11-14 07:26:26 +00:00
pablohashescobar 32a8133a9f dev: auto instance registration script 2023-11-14 07:23:18 +00:00
pablohashescobar efbf834e77 Merge branch 'feat/self_hosted_instance' of github.com:makeplane/plane into feat/self_hosted_instance 2023-11-13 20:29:44 +05:30
pablohashescobar 4fdd6a0e26 dev: instance configuration on instance bootup 2023-11-13 20:29:09 +05:30
pablohashescobar 081e42ced0 Merge branch 'feat/self_hosted_instance' of github.com:makeplane/plane into feat/self_hosted_instance 2023-11-09 07:03:20 +00:00
pablohashescobar 216f07f9d8 dev: email configuration settings moved to database 2023-11-09 12:32:34 +05:30
pablohashescobar 589e29ee45 Merge branch 'develop' of github.com:makeplane/plane into feat/self_hosted_instance 2023-11-09 06:16:05 +00:00
pablohashescobar b90218580d dev: update instance configuration load 2023-11-08 14:21:26 +00:00
pablohashescobar f37bae9ad9 Merge branch 'feat/self_hosted_instance' of github.com:makeplane/plane into feat/self_hosted_instance 2023-11-08 14:13:50 +00:00
pablohashescobar 7eaf52bae5 dev: set default values 2023-11-08 19:43:09 +05:30
pablohashescobar 3f78c1bbc0 dev: instance configuration variables 2023-11-08 13:56:52 +00:00
pablohashescobar 66cb297587 dev: update instance configuration model to take null and empty values 2023-11-08 19:01:45 +05:30
pablohashescobar a4f28b32f5 dev: instance configuration migration 2023-11-08 16:45:41 +05:30
pablohashescobar 39f69b7ed2 Merge branch 'feat/self_hosted_instance' of github.com:makeplane/plane into feat/self_hosted_instance 2023-11-08 11:14:26 +00:00
pablohashescobar 485e3e6a0f dev: instance configuration 2023-11-08 11:13:04 +00:00
pablohashescobar ea54bbca49 dev: reset migrations 2023-11-08 12:20:00 +05:30
pablohashescobar f34d508a48 dev: add default properties and issue mention migration 2023-11-08 12:16:01 +05:30
pablohashescobar 02a8caaabc Merge branch 'develop' of github.com:makeplane/plane into feat/self_hosted_instance 2023-11-08 12:12:50 +05:30
pablohashescobar 97b7a27695 dev: change license response structure 2023-10-30 20:17:30 +05:30
pablohashescobar 088615f9d6 dev: instance licenses 2023-10-30 17:38:18 +05:30
pablohashescobar b3868423f6 feat: self hosted licensing initialize 2023-10-30 17:01:30 +05:30
pablohashescobar 1fde71c633 dev: remove migration file 0046 2023-10-30 16:33:17 +05:30
pablohashescobar 5b2a1dda72 dev: initiate licensing 2023-10-30 16:32:51 +05:30
pablohashescobar 3bfbc3a132 dev: remove default user 2023-10-30 16:28:32 +05:30
236 changed files with 1977 additions and 5049 deletions
+4 -11
View File
@@ -3,17 +3,10 @@ set -e
python manage.py wait_for_db
python manage.py migrate
# Set default value for ENABLE_REGISTRATION
ENABLE_REGISTRATION=${ENABLE_REGISTRATION:-1}
# Check if ENABLE_REGISTRATION is not set to '0'
if [ "$ENABLE_REGISTRATION" != "0" ]; then
# Register instance
python manage.py register_instance
# Load the configuration variable
python manage.py configure_instance
fi
# Register instance
python manage.py register_instance
# Load the configuration variable
python manage.py configure_instance
# Create the default bucket
python bin/bucket_script.py
-8
View File
@@ -1,8 +0,0 @@
# from django.contrib import admin
from django.urls import path
from plane.analytics.views import EventTrackingEndpoint
urlpatterns = [
path('track-event/', EventTrackingEndpoint.as_view(), name='event_tracking'),
]
@@ -1 +0,0 @@
from .event_tacking import EventTrackingEndpoint
@@ -1,52 +0,0 @@
# Python imports
import requests
import json
# Module imports
from plane.app.views import BaseAPIView
# Third party imports
from rest_framework import status
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
# Django imports
from django.conf import settings
from django.core.serializers.json import DjangoJSONEncoder
class EventTrackingEndpoint(BaseAPIView):
permission_class = [
IsAuthenticated,
]
def post(self, request):
event_name = request.data.get("event_name", None)
extra = request.data.get("extra", None)
if not event_name or not extra:
return Response(
{"error": "event name and extra are required"},
status=status.HTTP_400_BAD_REQUEST,
)
_ = requests.post(
settings.ANALYTICS_BASE_API,
params={"token": settings.ANALYTICS_SECRET_KEY},
json=json.dumps(
{
"eventName": event_name,
"extra": extra,
"user": {
"id": str(request.user.id),
"email": str(request.user.email),
"first_name": str(request.user.first_name),
"last_name": str(request.user.last_name),
"last_login_ip": str(request.META.get("REMOTE_ADDR")),
"last_login_uagent": str(request.META.get("HTTP_USER_AGENT"))
},
},
cls=DjangoJSONEncoder,
),
)
return Response({"message": "success"}, status=status.HTTP_200_OK)
+5
View File
@@ -0,0 +1,5 @@
from django.apps import AppConfig
class ApiConfig(AppConfig):
name = "plane.api"
@@ -2,7 +2,7 @@
from .base import BaseSerializer
from plane.db.models import Estimate, EstimatePoint
from plane.app.serializers import WorkspaceLiteSerializer, ProjectLiteSerializer
from plane.api.serializers import WorkspaceLiteSerializer, ProjectLiteSerializer
class EstimateSerializer(BaseSerializer):
@@ -1,5 +1,5 @@
# Module imports
from plane.app.serializers import BaseSerializer
from plane.api.serializers import BaseSerializer
from plane.db.models import Integration, WorkspaceIntegration
@@ -1,5 +1,5 @@
# Module imports
from plane.app.serializers import BaseSerializer
from plane.api.serializers import BaseSerializer
from plane.db.models import (
GithubIssueSync,
GithubRepository,
@@ -1,5 +1,5 @@
# Module imports
from plane.app.serializers import BaseSerializer
from plane.api.serializers import BaseSerializer
from plane.db.models import SlackProjectSync
@@ -505,7 +505,7 @@ class IssueStateFlatSerializer(BaseSerializer):
# Issue Serializer with state details
class IssueStateSerializer(DynamicBaseSerializer):
class IssueStateSerializer(BaseSerializer):
label_details = LabelLiteSerializer(read_only=True, source="labels", many=True)
state_detail = StateLiteSerializer(read_only=True, source="state")
project_detail = ProjectLiteSerializer(read_only=True, source="project")
@@ -3,8 +3,8 @@ from rest_framework import serializers
# Module imports
from .base import BaseSerializer, DynamicBaseSerializer
from plane.app.serializers.workspace import WorkspaceLiteSerializer
from plane.app.serializers.user import UserLiteSerializer, UserAdminLiteSerializer
from plane.api.serializers.workspace import WorkSpaceSerializer, WorkspaceLiteSerializer
from plane.api.serializers.user import UserLiteSerializer, UserAdminLiteSerializer
from plane.db.models import (
Project,
ProjectMember,
@@ -1,6 +1,7 @@
# Module imports
from .base import BaseSerializer
from .workspace import WorkspaceLiteSerializer
from .project import ProjectLiteSerializer
from plane.db.models import State
@@ -13,6 +13,7 @@ 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
@@ -22,6 +23,10 @@ from .api import urlpatterns as api_urls
from .webhook import urlpatterns as webhook_urls
# Django imports
from django.conf import settings
urlpatterns = [
*analytic_urls,
*asset_urls,
@@ -38,6 +43,7 @@ urlpatterns = [
*notification_urls,
*page_urls,
*project_urls,
*public_board_urls,
*search_urls,
*state_urls,
*user_urls,
@@ -1,7 +1,7 @@
from django.urls import path
from plane.app.views import (
from plane.api.views import (
AnalyticsEndpoint,
AnalyticViewViewset,
SavedAnalyticEndpoint,
@@ -1,5 +1,5 @@
from django.urls import path
from plane.app.views import ApiTokenEndpoint
from plane.api.views import ApiTokenEndpoint
urlpatterns = [
# API Tokens
@@ -1,7 +1,7 @@
from django.urls import path
from plane.app.views import (
from plane.api.views import (
FileAssetEndpoint,
UserAssetsEndpoint,
)
@@ -3,7 +3,7 @@ from django.urls import path
from rest_framework_simplejwt.views import TokenRefreshView
from plane.app.views import (
from plane.api.views import (
# Authentication
SignUpEndpoint,
SignInEndpoint,
@@ -1,7 +1,7 @@
from django.urls import path
from plane.app.views import ConfigurationEndpoint
from plane.api.views import ConfigurationEndpoint
urlpatterns = [
path(
@@ -1,13 +1,12 @@
from django.urls import path
from plane.app.views import (
from plane.api.views import (
CycleViewSet,
CycleIssueViewSet,
CycleDateCheckEndpoint,
CycleFavoriteViewSet,
TransferCycleIssueEndpoint,
CycleIssueGroupedEndpoint,
)
@@ -44,11 +43,6 @@ urlpatterns = [
),
name="project-issue-cycle",
),
path(
"v3/workspaces/<str:slug>/projects/<uuid:project_id>/cycles/<uuid:cycle_id>/cycle-issues/",
CycleIssueGroupedEndpoint.as_view(),
name="project-issue-cycle",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/cycles/<uuid:cycle_id>/cycle-issues/<uuid:pk>/",
CycleIssueViewSet.as_view(
@@ -1,7 +1,7 @@
from django.urls import path
from plane.app.views import (
from plane.api.views import (
ProjectEstimatePointEndpoint,
BulkEstimatePointEndpoint,
)
@@ -1,9 +1,9 @@
from django.urls import path
from plane.app.views import UnsplashEndpoint
from plane.app.views import ReleaseNotesEndpoint
from plane.app.views import GPTIntegrationEndpoint
from plane.api.views import UnsplashEndpoint
from plane.api.views import ReleaseNotesEndpoint
from plane.api.views import GPTIntegrationEndpoint
urlpatterns = [
@@ -1,7 +1,7 @@
from django.urls import path
from plane.app.views import (
from plane.api.views import (
ServiceIssueImportSummaryEndpoint,
ImportServiceEndpoint,
UpdateServiceImportStatusEndpoint,
@@ -1,7 +1,7 @@
from django.urls import path
from plane.app.views import (
from plane.api.views import (
InboxViewSet,
InboxIssueViewSet,
)
@@ -1,7 +1,7 @@
from django.urls import path
from plane.app.views import (
from plane.api.views import (
IntegrationViewSet,
WorkspaceIntegrationViewSet,
GithubRepositoriesEndpoint,
@@ -1,7 +1,7 @@
from django.urls import path
from plane.app.views import (
from plane.api.views import (
IssueViewSet,
IssueListEndpoint,
IssueListGroupedEndpoint,
@@ -1,13 +1,12 @@
from django.urls import path
from plane.app.views import (
from plane.api.views import (
ModuleViewSet,
ModuleIssueViewSet,
ModuleLinkViewSet,
ModuleFavoriteViewSet,
BulkImportModulesEndpoint,
ModuleIssueGroupedEndpoint,
)
@@ -44,11 +43,6 @@ urlpatterns = [
),
name="project-module-issues",
),
path(
"v3/workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/module-issues/",
ModuleIssueGroupedEndpoint.as_view(),
name="project-issue-cycle",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/modules/<uuid:module_id>/module-issues/<uuid:pk>/",
ModuleIssueViewSet.as_view(
@@ -1,7 +1,7 @@
from django.urls import path
from plane.app.views import (
from plane.api.views import (
NotificationViewSet,
UnreadNotificationEndpoint,
MarkAllReadNotificationViewSet,
@@ -1,7 +1,7 @@
from django.urls import path
from plane.app.views import (
from plane.api.views import (
PageViewSet,
PageFavoriteViewSet,
PageLogEndpoint,
@@ -1,6 +1,6 @@
from django.urls import path
from plane.app.views import (
from plane.api.views import (
ProjectViewSet,
ProjectInvitationsViewset,
ProjectMemberViewSet,
@@ -10,9 +10,8 @@ from plane.app.views import (
ProjectUserViewsEndpoint,
ProjectIdentifierEndpoint,
ProjectFavoritesViewSet,
UserProjectInvitationsViewset,
ProjectPublicCoverImagesEndpoint,
ProjectDeployBoardViewSet,
UserProjectInvitationsViewset,
)
@@ -148,25 +147,4 @@ urlpatterns = [
ProjectPublicCoverImagesEndpoint.as_view(),
name="project-covers",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/project-deploy-boards/",
ProjectDeployBoardViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-deploy-board",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/project-deploy-boards/<uuid:pk>/",
ProjectDeployBoardViewSet.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="project-deploy-board",
),
]
+151
View File
@@ -0,0 +1,151 @@
from django.urls import path
from plane.api.views import (
ProjectDeployBoardViewSet,
ProjectDeployBoardPublicSettingsEndpoint,
ProjectIssuesPublicEndpoint,
IssueRetrievePublicEndpoint,
IssueCommentPublicViewSet,
IssueReactionPublicViewSet,
CommentReactionPublicViewSet,
InboxIssuePublicViewSet,
IssueVotePublicViewSet,
WorkspaceProjectDeployBoardEndpoint,
)
urlpatterns = [
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/project-deploy-boards/",
ProjectDeployBoardViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-deploy-board",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/project-deploy-boards/<uuid:pk>/",
ProjectDeployBoardViewSet.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="project-deploy-board",
),
path(
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/settings/",
ProjectDeployBoardPublicSettingsEndpoint.as_view(),
name="project-deploy-board-settings",
),
path(
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/issues/",
ProjectIssuesPublicEndpoint.as_view(),
name="project-deploy-board",
),
path(
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/issues/<uuid:issue_id>/",
IssueRetrievePublicEndpoint.as_view(),
name="workspace-project-boards",
),
path(
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/issues/<uuid:issue_id>/comments/",
IssueCommentPublicViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="issue-comments-project-board",
),
path(
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/issues/<uuid:issue_id>/comments/<uuid:pk>/",
IssueCommentPublicViewSet.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="issue-comments-project-board",
),
path(
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/issues/<uuid:issue_id>/reactions/",
IssueReactionPublicViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="issue-reactions-project-board",
),
path(
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/issues/<uuid:issue_id>/reactions/<str:reaction_code>/",
IssueReactionPublicViewSet.as_view(
{
"delete": "destroy",
}
),
name="issue-reactions-project-board",
),
path(
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/comments/<uuid:comment_id>/reactions/",
CommentReactionPublicViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="comment-reactions-project-board",
),
path(
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/comments/<uuid:comment_id>/reactions/<str:reaction_code>/",
CommentReactionPublicViewSet.as_view(
{
"delete": "destroy",
}
),
name="comment-reactions-project-board",
),
path(
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/inboxes/<uuid:inbox_id>/inbox-issues/",
InboxIssuePublicViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="inbox-issue",
),
path(
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/inboxes/<uuid:inbox_id>/inbox-issues/<uuid:pk>/",
InboxIssuePublicViewSet.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="inbox-issue",
),
path(
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/issues/<uuid:issue_id>/votes/",
IssueVotePublicViewSet.as_view(
{
"get": "list",
"post": "create",
"delete": "destroy",
}
),
name="issue-vote-project-board",
),
path(
"public/workspaces/<str:slug>/project-boards/",
WorkspaceProjectDeployBoardEndpoint.as_view(),
name="workspace-project-boards",
),
]
@@ -1,7 +1,7 @@
from django.urls import path
from plane.app.views import (
from plane.api.views import (
GlobalSearchEndpoint,
IssueSearchEndpoint,
)
@@ -1,7 +1,7 @@
from django.urls import path
from plane.app.views import StateViewSet
from plane.api.views import StateViewSet
urlpatterns = [
@@ -1,6 +1,6 @@
from django.urls import path
from plane.app.views import (
from plane.api.views import (
## User
UserEndpoint,
UpdateUserOnBoardedEndpoint,
@@ -1,7 +1,7 @@
from django.urls import path
from plane.app.views import (
from plane.api.views import (
IssueViewViewSet,
GlobalViewViewSet,
GlobalViewIssuesViewSet,
@@ -1,6 +1,6 @@
from django.urls import path
from plane.app.views import (
from plane.api.views import (
WebhookEndpoint,
WebhookLogsEndpoint,
WebhookSecretRegenerateEndpoint,
@@ -1,7 +1,7 @@
from django.urls import path
from plane.app.views import (
from plane.api.views import (
UserWorkspaceInvitationsViewSet,
WorkSpaceViewSet,
WorkspaceJoinEndpoint,
@@ -4,7 +4,7 @@ from rest_framework_simplejwt.views import TokenRefreshView
# Create your urls here.
from plane.app.views import (
from plane.api.views import (
# Authentication
SignUpEndpoint,
SignInEndpoint,
@@ -9,8 +9,10 @@ from .project import (
ProjectUserViewsEndpoint,
ProjectMemberUserEndpoint,
ProjectFavoritesViewSet,
ProjectPublicCoverImagesEndpoint,
ProjectDeployBoardViewSet,
ProjectDeployBoardPublicSettingsEndpoint,
WorkspaceProjectDeployBoardEndpoint,
ProjectPublicCoverImagesEndpoint,
)
from .user import (
UserEndpoint,
@@ -58,7 +60,6 @@ from .cycle import (
CycleDateCheckEndpoint,
CycleFavoriteViewSet,
TransferCycleIssueEndpoint,
CycleIssueGroupedEndpoint,
)
from .asset import FileAssetEndpoint, UserAssetsEndpoint
from .issue import (
@@ -78,9 +79,15 @@ from .issue import (
IssueAttachmentEndpoint,
IssueArchiveViewSet,
IssueSubscriberViewSet,
IssueCommentPublicViewSet,
CommentReactionViewSet,
IssueReactionViewSet,
IssueReactionPublicViewSet,
CommentReactionPublicViewSet,
IssueVotePublicViewSet,
IssueRelationViewSet,
IssueRetrievePublicEndpoint,
ProjectIssuesPublicEndpoint,
IssueDraftViewSet,
)
@@ -106,7 +113,6 @@ from .module import (
ModuleIssueViewSet,
ModuleLinkViewSet,
ModuleFavoriteViewSet,
ModuleIssueGroupedEndpoint,
)
from .api import ApiTokenEndpoint
@@ -148,7 +154,7 @@ from .estimate import (
BulkEstimatePointEndpoint,
)
from .inbox import InboxViewSet, InboxIssueViewSet
from .inbox import InboxViewSet, InboxIssueViewSet, InboxIssuePublicViewSet
from .analytic import (
AnalyticsEndpoint,
@@ -5,12 +5,13 @@ 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.app.views import BaseAPIView, BaseViewSet
from plane.app.permissions import WorkSpaceAdminPermission
from plane.api.views import BaseAPIView, BaseViewSet
from plane.api.permissions import WorkSpaceAdminPermission
from plane.db.models import Issue, AnalyticView, Workspace, State, Label
from plane.app.serializers import AnalyticViewSerializer
from plane.api.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
@@ -8,8 +8,8 @@ from rest_framework import status
# Module import
from .base import BaseAPIView
from plane.db.models import APIToken, Workspace
from plane.app.serializers import APITokenSerializer, APITokenReadSerializer
from plane.app.permissions import WorkspaceOwnerPermission
from plane.api.serializers import APITokenSerializer, APITokenReadSerializer
from plane.api.permissions import WorkspaceOwnerPermission
class ApiTokenEndpoint(BaseAPIView):
@@ -2,11 +2,12 @@
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.app.serializers import FileAssetSerializer
from plane.api.serializers import FileAssetSerializer
class FileAssetEndpoint(BaseAPIView):
@@ -21,7 +21,7 @@ from sentry_sdk import capture_exception
## Module imports
from . import BaseAPIView
from plane.app.serializers import (
from plane.api.serializers import (
ChangePasswordSerializer,
ResetPasswordSerializer,
)
@@ -5,7 +5,6 @@ import string
import json
import requests
from requests.exceptions import RequestException
# Django imports
from django.utils import timezone
from django.core.exceptions import ValidationError
@@ -1,5 +1,6 @@
# Python imports
import zoneinfo
import json
# Django imports
from django.urls import resolve
@@ -7,6 +8,7 @@ from django.conf import settings
from django.utils import timezone
from django.db import IntegrityError
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.core.serializers.json import DjangoJSONEncoder
# Third part imports
from rest_framework import status
@@ -22,6 +24,7 @@ from django_filters.rest_framework import DjangoFilterBackend
# Module imports
from plane.utils.paginator import BasePaginator
from plane.bgtasks.webhook_task import send_webhook
class TimezoneMixin:
@@ -38,6 +41,27 @@ class TimezoneMixin:
timezone.deactivate()
class WebhookMixin:
webhook_event = None
def finalize_response(self, request, response, *args, **kwargs):
response = super().finalize_response(request, response, *args, **kwargs)
if (
self.webhook_event
and self.request.method in ["POST", "PATCH", "DELETE"]
and response.status_code in [200, 201, 204]
):
send_webhook.delay(
event=self.webhook_event,
event_data=json.dumps(response.data, cls=DjangoJSONEncoder),
action=self.request.method,
slug=self.workspace_slug,
)
return response
class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator):
model = None
@@ -8,6 +8,7 @@ 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
@@ -20,17 +20,18 @@ 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.app.serializers import (
from plane.api.serializers import (
CycleSerializer,
CycleIssueSerializer,
CycleFavoriteSerializer,
IssueStateSerializer,
CycleWriteSerializer,
)
from plane.app.permissions import ProjectEntityPermission
from plane.api.permissions import ProjectEntityPermission
from plane.db.models import (
User,
Cycle,
@@ -706,56 +707,6 @@ class CycleIssueViewSet(WebhookMixin, BaseViewSet):
return Response(status=status.HTTP_204_NO_CONTENT)
class CycleIssueGroupedEndpoint(BaseAPIView):
permission_classes = [
ProjectEntityPermission,
]
def get(self, request, slug, project_id, cycle_id):
filters = issue_filters(request.query_params, "GET")
fields = [field for field in request.GET.get("fields", "").split(",") if field]
issues = (
Issue.issue_objects.filter(issue_cycle__cycle_id=cycle_id)
.annotate(
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(bridge_id=F("issue_cycle__id"))
.filter(project_id=project_id)
.filter(workspace__slug=slug)
.select_related("project")
.select_related("workspace")
.select_related("state")
.select_related("parent")
.prefetch_related("assignees")
.prefetch_related("labels")
.filter(**filters)
.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")
)
)
issues = IssueStateSerializer(issues, many=True, fields=fields if fields else None).data
issue_dict = {str(issue["id"]): issue for issue in issues}
return Response(
issue_dict,
status=status.HTTP_200_OK,
)
class CycleDateCheckEndpoint(BaseAPIView):
permission_classes = [
ProjectEntityPermission,
@@ -1,12 +1,13 @@
# 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.app.permissions import ProjectEntityPermission
from plane.api.permissions import ProjectEntityPermission
from plane.db.models import Project, Estimate, EstimatePoint
from plane.app.serializers import (
from plane.api.serializers import (
EstimateSerializer,
EstimatePointSerializer,
EstimateReadSerializer,
@@ -1,14 +1,15 @@
# 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.app.permissions import WorkSpaceAdminPermission
from plane.api.permissions import WorkSpaceAdminPermission
from plane.bgtasks.export_task import issue_export_task
from plane.db.models import Project, ExporterHistory, Workspace
from plane.app.serializers import ExporterHistorySerializer
from plane.api.serializers import ExporterHistorySerializer
class ExportIssuesEndpoint(BaseAPIView):
@@ -5,15 +5,17 @@ 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.app.permissions import ProjectEntityPermission
from plane.api.permissions import ProjectEntityPermission
from plane.db.models import Workspace, Project
from plane.app.serializers import ProjectLiteSerializer, WorkspaceLiteSerializer
from plane.api.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
@@ -4,12 +4,13 @@ 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.app.views import BaseAPIView
from plane.api.views import BaseAPIView
from plane.db.models import (
WorkspaceIntegration,
Importer,
@@ -29,7 +30,7 @@ from plane.db.models import (
ModuleIssue,
Label,
)
from plane.app.serializers import (
from plane.api.serializers import (
ImporterSerializer,
IssueFlatSerializer,
ModuleSerializer,
@@ -38,7 +39,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.app.permissions import WorkSpaceAdminPermission
from plane.api.permissions import WorkSpaceAdminPermission
class ServiceIssueImportSummaryEndpoint(BaseAPIView):
@@ -9,10 +9,11 @@ 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.app.permissions import ProjectBasePermission, ProjectLitePermission
from plane.api.permissions import ProjectBasePermission, ProjectLitePermission
from plane.db.models import (
Inbox,
InboxIssue,
@@ -21,8 +22,9 @@ from plane.db.models import (
IssueLink,
IssueAttachment,
ProjectMember,
ProjectDeployBoard,
)
from plane.app.serializers import (
from plane.api.serializers import (
IssueSerializer,
InboxSerializer,
InboxIssueSerializer,
@@ -357,3 +359,253 @@ 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", "<p></p>"
),
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)
@@ -10,7 +10,7 @@ from rest_framework import status
from sentry_sdk import capture_exception
# Module imports
from plane.app.views import BaseViewSet
from plane.api.views import BaseViewSet
from plane.db.models import (
Integration,
WorkspaceIntegration,
@@ -19,12 +19,12 @@ from plane.db.models import (
WorkspaceMember,
APIToken,
)
from plane.app.serializers import IntegrationSerializer, WorkspaceIntegrationSerializer
from plane.api.serializers import IntegrationSerializer, WorkspaceIntegrationSerializer
from plane.utils.integrations.github import (
get_github_metadata,
delete_github_installation,
)
from plane.app.permissions import WorkSpaceAdminPermission
from plane.api.permissions import WorkSpaceAdminPermission
from plane.utils.integrations.slack import slack_oauth
class IntegrationViewSet(BaseViewSet):
@@ -4,7 +4,7 @@ from rest_framework.response import Response
from sentry_sdk import capture_exception
# Module imports
from plane.app.views import BaseViewSet, BaseAPIView
from plane.api.views import BaseViewSet, BaseAPIView
from plane.db.models import (
GithubIssueSync,
GithubRepositorySync,
@@ -15,13 +15,13 @@ from plane.db.models import (
GithubCommentSync,
Project,
)
from plane.app.serializers import (
from plane.api.serializers import (
GithubIssueSyncSerializer,
GithubRepositorySyncSerializer,
GithubCommentSyncSerializer,
)
from plane.utils.integrations.github import get_github_repos
from plane.app.permissions import ProjectBasePermission, ProjectEntityPermission
from plane.api.permissions import ProjectBasePermission, ProjectEntityPermission
class GithubRepositoriesEndpoint(BaseAPIView):
@@ -7,10 +7,10 @@ from rest_framework.response import Response
from sentry_sdk import capture_exception
# Module imports
from plane.app.views import BaseViewSet, BaseAPIView
from plane.api.views import BaseViewSet, BaseAPIView
from plane.db.models import SlackProjectSync, WorkspaceIntegration, ProjectMember
from plane.app.serializers import SlackProjectSyncSerializer
from plane.app.permissions import ProjectBasePermission, ProjectEntityPermission
from plane.api.serializers import SlackProjectSyncSerializer
from plane.api.permissions import ProjectBasePermission, ProjectEntityPermission
from plane.utils.integrations.slack import slack_oauth
@@ -29,10 +29,12 @@ 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.app.serializers import (
from plane.api.serializers import (
IssueCreateSerializer,
IssueActivitySerializer,
IssueCommentSerializer,
@@ -52,7 +54,7 @@ from plane.app.serializers import (
RelatedIssueSerializer,
IssuePublicSerializer,
)
from plane.app.permissions import (
from plane.api.permissions import (
ProjectEntityPermission,
WorkSpaceAdminPermission,
ProjectMemberPermission,
@@ -1460,6 +1462,432 @@ 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
@@ -1545,6 +1973,182 @@ 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,
@@ -3,6 +3,7 @@ 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
@@ -11,10 +12,11 @@ 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.app.serializers import (
from . import BaseViewSet, WebhookMixin
from plane.api.serializers import (
ModuleWriteSerializer,
ModuleSerializer,
ModuleIssueSerializer,
@@ -22,7 +24,7 @@ from plane.app.serializers import (
ModuleFavoriteSerializer,
IssueStateSerializer,
)
from plane.app.permissions import ProjectEntityPermission
from plane.api.permissions import ProjectEntityPermission
from plane.db.models import (
Module,
ModuleIssue,
@@ -479,55 +481,6 @@ class ModuleIssueViewSet(BaseViewSet):
return Response(status=status.HTTP_204_NO_CONTENT)
class ModuleIssueGroupedEndpoint(BaseAPIView):
permission_classes = [
ProjectEntityPermission,
]
def get(self, request, slug, project_id, module_id):
filters = issue_filters(request.query_params, "GET")
fields = [field for field in request.GET.get("fields", "").split(",") if field]
issues = (
Issue.issue_objects.filter(issue_module__module_id=module_id)
.annotate(
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(bridge_id=F("issue_module__id"))
.filter(project_id=project_id)
.filter(workspace__slug=slug)
.select_related("project")
.select_related("workspace")
.select_related("state")
.select_related("parent")
.prefetch_related("assignees")
.prefetch_related("labels")
.filter(**filters)
.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")
)
)
issues = IssueStateSerializer(issues, many=True, fields=fields if fields else None).data
issue_dict = {str(issue["id"]): issue for issue in issues}
return Response(
issue_dict,
status=status.HTTP_200_OK,
)
class ModuleLinkViewSet(BaseViewSet):
permission_classes = [
ProjectEntityPermission,
@@ -5,6 +5,7 @@ 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
@@ -16,7 +17,7 @@ from plane.db.models import (
Issue,
WorkspaceMember,
)
from plane.app.serializers import NotificationSerializer
from plane.api.serializers import NotificationSerializer
class NotificationViewSet(BaseViewSet, BasePaginator):
@@ -29,6 +29,7 @@ from plane.db.models import (
ProjectMemberInvite,
ProjectMember,
)
from plane.api.serializers import UserSerializer
from .base import BaseAPIView
@@ -3,18 +3,26 @@ from datetime import timedelta, date, datetime
# Django imports
from django.db import connection
from django.db.models import Exists, OuterRef, Q
from django.db.models import Exists, OuterRef, Q, Prefetch
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.app.permissions import ProjectEntityPermission
from plane.api.permissions import ProjectEntityPermission
from plane.db.models import (
Page,
PageFavorite,
@@ -23,7 +31,7 @@ from plane.db.models import (
IssueActivity,
PageLog,
)
from plane.app.serializers import (
from plane.api.serializers import (
PageSerializer,
PageFavoriteSerializer,
PageLogSerializer,
@@ -79,10 +87,15 @@ 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,
@@ -135,8 +148,10 @@ class PageViewSet(BaseViewSet):
status=status.HTTP_400_BAD_REQUEST,
)
def lock(self, request, slug, project_id, page_id):
page = Page.objects.get(pk=page_id, workspace__slug=slug, project_id=project_id)
def lock(self, request, slug, project_id, pk):
page = Page.objects.filter(
pk=pk, workspace__slug=slug, project_id=project_id
)
# only the owner can lock the page
if request.user.id != page.owned_by_id:
@@ -148,8 +163,8 @@ class PageViewSet(BaseViewSet):
page.save()
return Response(status=status.HTTP_204_NO_CONTENT)
def unlock(self, request, slug, project_id, page_id):
page = Page.objects.get(pk=page_id, workspace__slug=slug, project_id=project_id)
def unlock(self, request, slug, project_id, pk):
page = Page.objects.get(pk=pk, workspace__slug=slug, project_id=project_id)
# only the owner can unlock the page
if request.user.id != page.owned_by_id:
@@ -227,31 +242,27 @@ class PageViewSet(BaseViewSet):
)
def archive(self, request, slug, project_id, 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,
)
_ = Page.objects.get(
project_id=project_id,
owned_by_id=request.user.id,
workspace__slug=slug,
pk=page_id,
)
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(pk=page_id, workspace__slug=slug, project_id=project_id)
page = Page.objects.get(
project_id=project_id,
owned_by_id=request.user.id,
workspace__slug=slug,
pk=page_id,
)
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'])
page.parent = None
page.save()
unarchive_archive_page_and_descendants(page_id, None)
@@ -264,13 +275,20 @@ 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,
@@ -392,9 +410,11 @@ 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
)
)
@@ -26,8 +26,8 @@ from rest_framework import serializers
from rest_framework.permissions import AllowAny
# Module imports
from .base import BaseViewSet, BaseAPIView, WebhookMixin, AnalyticsMixin
from plane.app.serializers import (
from .base import BaseViewSet, BaseAPIView, WebhookMixin
from plane.api.serializers import (
ProjectSerializer,
ProjectListSerializer,
ProjectMemberSerializer,
@@ -38,9 +38,12 @@ from plane.app.serializers import (
ProjectMemberAdminSerializer,
)
from plane.app.permissions import (
from plane.api.permissions import (
WorkspaceUserPermission,
ProjectBasePermission,
ProjectEntityPermission,
ProjectMemberPermission,
ProjectLitePermission,
)
from plane.db.models import (
@@ -64,11 +67,10 @@ from plane.db.models import (
from plane.bgtasks.project_invitation_task import project_invitation
class ProjectViewSet(WebhookMixin, AnalyticsMixin, BaseViewSet):
class ProjectViewSet(WebhookMixin, BaseViewSet):
serializer_class = ProjectSerializer
model = Project
webhook_event = "project"
analytic_event = "project"
permission_classes = [
ProjectBasePermission,
@@ -963,37 +965,6 @@ 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,
@@ -1041,4 +1012,77 @@ class ProjectDeployBoardViewSet(BaseViewSet):
project_deploy_board.save()
serializer = ProjectDeployBoardSerializer(project_deploy_board)
return Response(serializer.data, status=status.HTTP_200_OK)
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)
@@ -7,6 +7,7 @@ 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
@@ -7,11 +7,12 @@ 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
from plane.app.serializers import StateSerializer
from plane.app.permissions import ProjectEntityPermission
from . import BaseViewSet, BaseAPIView
from plane.api.serializers import StateSerializer
from plane.api.permissions import ProjectEntityPermission
from plane.db.models import State, Issue
@@ -2,16 +2,17 @@
from rest_framework.response import Response
from rest_framework import status
from sentry_sdk import capture_exception
# Module imports
from plane.app.serializers import (
from plane.api.serializers import (
UserSerializer,
IssueActivitySerializer,
UserMeSerializer,
UserMeSettingsSerializer,
)
from plane.app.views.base import BaseViewSet, BaseAPIView
from plane.api.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
@@ -18,16 +18,17 @@ 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
from plane.app.serializers import (
from . import BaseViewSet, BaseAPIView
from plane.api.serializers import (
GlobalViewSerializer,
IssueViewSerializer,
IssueLiteSerializer,
IssueViewFavoriteSerializer,
)
from plane.app.permissions import WorkspaceEntityPermission, ProjectEntityPermission
from plane.api.permissions import WorkspaceEntityPermission, ProjectEntityPermission
from plane.db.models import (
Workspace,
GlobalView,
@@ -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.app.permissions import WorkspaceOwnerPermission
from plane.app.serializers import WebhookSerializer, WebhookLogSerializer
from plane.api.permissions import WorkspaceOwnerPermission
from plane.api.serializers import WebhookSerializer, WebhookLogSerializer
class WebhookEndpoint(BaseAPIView):
@@ -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
from rest_framework.permissions import AllowAny, IsAuthenticated
# Module imports
from plane.app.serializers import (
from plane.api.serializers import (
WorkSpaceSerializer,
WorkSpaceMemberSerializer,
TeamSerializer,
@@ -45,7 +45,7 @@ from plane.app.serializers import (
WorkspaceMemberAdminSerializer,
WorkspaceMemberMeSerializer,
)
from plane.app.views.base import BaseAPIView
from plane.api.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.app.permissions import (
from plane.api.permissions import (
WorkSpaceBasePermission,
WorkSpaceAdminPermission,
WorkspaceEntityPermission,
-5
View File
@@ -1,5 +0,0 @@
from django.apps import AppConfig
class AppConfig(AppConfig):
name = "plane.app"
-260
View File
@@ -1,260 +0,0 @@
# Python imports
import zoneinfo
import json
# 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
from django.core.serializers.json import DjangoJSONEncoder
# 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
from plane.bgtasks.webhook_task import send_webhook
from plane.bgtasks.track_events import track_event
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 WebhookMixin:
webhook_event = None
def initial_request(self, request, response, *args, **kwargs):
response = super().initial_request(request, response, *args, **kwargs)
if (
self.webhook_event
and self.request.method in ["POST", "PATCH", "DELETE"]
and response.status_code in [200, 201, 204]
):
send_webhook.delay(
event=self.webhook_event,
event_data=json.dumps(response.data, cls=DjangoJSONEncoder),
action=self.request.method,
slug=self.workspace_slug,
)
return response
class AnalyticsMixin:
analytic_event = None
print("did it come here or not")
def initial(self, request, *args, **kwargs):
super().initial(request, *args, **kwargs)
print("hieieie")
print(self.workspace_slug,"workspace_slug")
# if (
# self.analytic_event
# and self.request.method in ["POST", "PATCH", "DELETE"]
# and response.status_code in [200, 201, 204]
# ):
# print(self.workspace_slug,"workspace_slug")
# track_event.delay(
# event_name=self.analytic_event,
# slug=self.workspace_slug,
# project_id=self.project_id,
# user_id=self.request.user.id,
# )
# return response
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)
+1 -1
View File
@@ -13,7 +13,7 @@ from celery import shared_task
from sentry_sdk import capture_exception
# Module imports
from plane.app.serializers import ImporterSerializer
from plane.api.serializers import ImporterSerializer
from plane.db.models import (
Importer,
WorkspaceMember,
@@ -21,11 +21,14 @@ from plane.db.models import (
State,
Cycle,
Module,
IssueSubscriber,
Notification,
IssueAssignee,
IssueReaction,
CommentReaction,
IssueComment,
)
from plane.app.serializers import IssueActivitySerializer
from plane.api.serializers import IssueActivitySerializer
from plane.bgtasks.notification_task import notifications
-55
View File
@@ -1,55 +0,0 @@
import requests
import json
# Django imports
from django.conf import settings
from django.core.serializers.json import DjangoJSONEncoder
# Module imports
from plane.db.models import Workspace, Project, User
# Third party imports
from celery import shared_task
from sentry_sdk import capture_exception
@shared_task
def track_event(event_name, slug, project_id, user_id):
try:
workspace = Workspace.objects.get(slug=slug)
project = Project.objects.get(slug=slug, id=project_id)
user = User.objects.get(id=user_id)
extra = {
"projectId": str(project.id),
"projectIdentifier": str(project.identifier),
"projectName": str(project.name),
"workspaceId": str(workspace.id),
"workspaceName": str(workspace.name),
"workspaceSlug": str(workspace.slug),
}
_ = requests.post(
settings.ANALYTICS_BASE_API,
params={"token": settings.ANALYTICS_SECRET_KEY},
json=json.dumps(
{
"eventName": event_name,
"extra": extra,
"user": {
"id": str(user.id),
"email": str(user.email),
"first_name": str(user.first_name),
"last_name": str(user.last_name),
"last_login_ip": str(user.last_login_ip),
"last_login_uagent": str(user.last_login_uagent),
},
},
cls=DjangoJSONEncoder,
),
)
except Exception as e:
if settings.DEBUG:
print(e)
capture_exception(e)
return
@@ -1,23 +0,0 @@
# Generated by Django 4.2.5 on 2023-11-17 08:48
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('db', '0049_auto_20231116_0713'),
]
operations = [
migrations.AddField(
model_name='user',
name='use_case',
field=models.TextField(blank=True, null=True),
),
migrations.AlterField(
model_name='workspace',
name='organization_size',
field=models.CharField(blank=True, max_length=20, null=True),
),
]
-1
View File
@@ -86,7 +86,6 @@ class User(AbstractBaseUser, PermissionsMixin):
display_name = models.CharField(max_length=255, default="")
is_tour_completed = models.BooleanField(default=False)
onboarding_step = models.JSONField(default=get_default_onboarding)
use_case = models.TextField(blank=True, null=True)
USERNAME_FIELD = "email"
+1 -1
View File
@@ -72,7 +72,7 @@ class Workspace(BaseModel):
related_name="owner_workspace",
)
slug = models.SlugField(max_length=48, db_index=True, unique=True)
organization_size = models.CharField(max_length=20, blank=True, null=True)
organization_size = models.CharField(max_length=20)
def __str__(self):
"""Return name of the Workspace"""
@@ -1,7 +1,7 @@
# Module imports
from plane.license.models import Instance, InstanceAdmin, InstanceConfiguration
from plane.app.serializers import BaseSerializer
from plane.app.serializers import UserAdminLiteSerializer
from plane.api.serializers import BaseSerializer
from plane.api.serializers import UserAdminLiteSerializer
class InstanceSerializer(BaseSerializer):
@@ -11,7 +11,7 @@ from rest_framework import status
from rest_framework.response import Response
# Module imports
from plane.app.views import BaseAPIView
from plane.api.views import BaseAPIView
from plane.license.models import Instance, InstanceAdmin, InstanceConfiguration
from plane.license.api.serializers import (
InstanceSerializer,
+2 -2
View File
@@ -27,6 +27,7 @@ DEBUG = False
# Allowed Hosts
ALLOWED_HOSTS = ["*"]
# Application definition
INSTALLED_APPS = [
"django.contrib.auth",
@@ -34,8 +35,7 @@ INSTALLED_APPS = [
"django.contrib.sessions",
# Inhouse apps
"plane.analytics",
"plane.app",
"plane.space",
"plane.api",
"plane.bgtasks",
"plane.db",
"plane.utils",
View File

Some files were not shown because too many files have changed in this diff Show More