forked from github/plane
[WEB-664] refactor: folder structure (#3884)
* refactor: folder structure * chore: resolved merge conflicts
This commit is contained in:
parent
b03f6a81e2
commit
bc02e56e3c
@ -3,14 +3,15 @@ from django.urls import path
|
|||||||
from plane.app.views import (
|
from plane.app.views import (
|
||||||
BulkCreateIssueLabelsEndpoint,
|
BulkCreateIssueLabelsEndpoint,
|
||||||
BulkDeleteIssuesEndpoint,
|
BulkDeleteIssuesEndpoint,
|
||||||
|
SubIssuesEndpoint,
|
||||||
|
IssueLinkViewSet,
|
||||||
|
IssueAttachmentEndpoint,
|
||||||
CommentReactionViewSet,
|
CommentReactionViewSet,
|
||||||
ExportIssuesEndpoint,
|
ExportIssuesEndpoint,
|
||||||
IssueActivityEndpoint,
|
IssueActivityEndpoint,
|
||||||
IssueArchiveViewSet,
|
IssueArchiveViewSet,
|
||||||
IssueAttachmentEndpoint,
|
|
||||||
IssueCommentViewSet,
|
IssueCommentViewSet,
|
||||||
IssueDraftViewSet,
|
IssueDraftViewSet,
|
||||||
IssueLinkViewSet,
|
|
||||||
IssueListEndpoint,
|
IssueListEndpoint,
|
||||||
IssueReactionViewSet,
|
IssueReactionViewSet,
|
||||||
IssueRelationViewSet,
|
IssueRelationViewSet,
|
||||||
@ -18,8 +19,6 @@ from plane.app.views import (
|
|||||||
IssueUserDisplayPropertyEndpoint,
|
IssueUserDisplayPropertyEndpoint,
|
||||||
IssueViewSet,
|
IssueViewSet,
|
||||||
LabelViewSet,
|
LabelViewSet,
|
||||||
SubIssuesEndpoint,
|
|
||||||
UserWorkSpaceIssues,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
@ -82,11 +81,6 @@ urlpatterns = [
|
|||||||
BulkDeleteIssuesEndpoint.as_view(),
|
BulkDeleteIssuesEndpoint.as_view(),
|
||||||
name="project-issues-bulk",
|
name="project-issues-bulk",
|
||||||
),
|
),
|
||||||
path(
|
|
||||||
"workspaces/<str:slug>/my-issues/",
|
|
||||||
UserWorkSpaceIssues.as_view(),
|
|
||||||
name="workspace-issues",
|
|
||||||
),
|
|
||||||
##
|
##
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/sub-issues/",
|
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/sub-issues/",
|
||||||
|
@ -1,19 +1,26 @@
|
|||||||
from .project import (
|
from .project.base import (
|
||||||
ProjectViewSet,
|
ProjectViewSet,
|
||||||
ProjectMemberViewSet,
|
|
||||||
UserProjectInvitationsViewset,
|
|
||||||
ProjectInvitationsViewset,
|
|
||||||
AddTeamToProjectEndpoint,
|
|
||||||
ProjectIdentifierEndpoint,
|
ProjectIdentifierEndpoint,
|
||||||
ProjectJoinEndpoint,
|
|
||||||
ProjectUserViewsEndpoint,
|
ProjectUserViewsEndpoint,
|
||||||
ProjectMemberUserEndpoint,
|
|
||||||
ProjectFavoritesViewSet,
|
ProjectFavoritesViewSet,
|
||||||
ProjectPublicCoverImagesEndpoint,
|
ProjectPublicCoverImagesEndpoint,
|
||||||
ProjectDeployBoardViewSet,
|
ProjectDeployBoardViewSet,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .project.invite import (
|
||||||
|
UserProjectInvitationsViewset,
|
||||||
|
ProjectInvitationsViewset,
|
||||||
|
ProjectJoinEndpoint,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .project.member import (
|
||||||
|
ProjectMemberViewSet,
|
||||||
|
AddTeamToProjectEndpoint,
|
||||||
|
ProjectMemberUserEndpoint,
|
||||||
UserProjectRolesEndpoint,
|
UserProjectRolesEndpoint,
|
||||||
)
|
)
|
||||||
from .user import (
|
|
||||||
|
from .user.base import (
|
||||||
UserEndpoint,
|
UserEndpoint,
|
||||||
UpdateUserOnBoardedEndpoint,
|
UpdateUserOnBoardedEndpoint,
|
||||||
UpdateUserTourCompletedEndpoint,
|
UpdateUserTourCompletedEndpoint,
|
||||||
@ -24,71 +31,121 @@ from .oauth import OauthEndpoint
|
|||||||
|
|
||||||
from .base import BaseAPIView, BaseViewSet, WebhookMixin
|
from .base import BaseAPIView, BaseViewSet, WebhookMixin
|
||||||
|
|
||||||
from .workspace import (
|
from .workspace.base import (
|
||||||
WorkSpaceViewSet,
|
WorkSpaceViewSet,
|
||||||
UserWorkSpacesEndpoint,
|
UserWorkSpacesEndpoint,
|
||||||
WorkSpaceAvailabilityCheckEndpoint,
|
WorkSpaceAvailabilityCheckEndpoint,
|
||||||
WorkspaceJoinEndpoint,
|
|
||||||
WorkSpaceMemberViewSet,
|
|
||||||
TeamMemberViewSet,
|
|
||||||
WorkspaceInvitationsViewset,
|
|
||||||
UserWorkspaceInvitationsViewSet,
|
|
||||||
UserLastProjectWithWorkspaceEndpoint,
|
|
||||||
WorkspaceMemberUserEndpoint,
|
|
||||||
WorkspaceMemberUserViewsEndpoint,
|
|
||||||
UserActivityGraphEndpoint,
|
|
||||||
UserIssueCompletedGraphEndpoint,
|
|
||||||
UserWorkspaceDashboardEndpoint,
|
UserWorkspaceDashboardEndpoint,
|
||||||
WorkspaceThemeViewSet,
|
WorkspaceThemeViewSet,
|
||||||
WorkspaceUserProfileStatsEndpoint,
|
ExportWorkspaceUserActivityEndpoint
|
||||||
WorkspaceUserActivityEndpoint,
|
)
|
||||||
WorkspaceUserProfileEndpoint,
|
|
||||||
WorkspaceUserProfileIssuesEndpoint,
|
from .workspace.member import (
|
||||||
WorkspaceLabelsEndpoint,
|
WorkSpaceMemberViewSet,
|
||||||
|
TeamMemberViewSet,
|
||||||
|
WorkspaceMemberUserEndpoint,
|
||||||
WorkspaceProjectMemberEndpoint,
|
WorkspaceProjectMemberEndpoint,
|
||||||
WorkspaceUserPropertiesEndpoint,
|
WorkspaceMemberUserViewsEndpoint,
|
||||||
|
)
|
||||||
|
from .workspace.invite import (
|
||||||
|
WorkspaceInvitationsViewset,
|
||||||
|
WorkspaceJoinEndpoint,
|
||||||
|
UserWorkspaceInvitationsViewSet,
|
||||||
|
)
|
||||||
|
from .workspace.label import (
|
||||||
|
WorkspaceLabelsEndpoint,
|
||||||
|
)
|
||||||
|
from .workspace.state import (
|
||||||
WorkspaceStatesEndpoint,
|
WorkspaceStatesEndpoint,
|
||||||
|
)
|
||||||
|
from .workspace.user import (
|
||||||
|
UserLastProjectWithWorkspaceEndpoint,
|
||||||
|
WorkspaceUserProfileIssuesEndpoint,
|
||||||
|
WorkspaceUserPropertiesEndpoint,
|
||||||
|
WorkspaceUserProfileEndpoint,
|
||||||
|
WorkspaceUserActivityEndpoint,
|
||||||
|
WorkspaceUserProfileStatsEndpoint,
|
||||||
|
UserActivityGraphEndpoint,
|
||||||
|
UserIssueCompletedGraphEndpoint,
|
||||||
|
)
|
||||||
|
from .workspace.estimate import (
|
||||||
WorkspaceEstimatesEndpoint,
|
WorkspaceEstimatesEndpoint,
|
||||||
ExportWorkspaceUserActivityEndpoint,
|
)
|
||||||
|
from .workspace.module import (
|
||||||
WorkspaceModulesEndpoint,
|
WorkspaceModulesEndpoint,
|
||||||
|
)
|
||||||
|
from .workspace.cycle import (
|
||||||
WorkspaceCyclesEndpoint,
|
WorkspaceCyclesEndpoint,
|
||||||
)
|
)
|
||||||
from .state import StateViewSet
|
|
||||||
from .view import (
|
from .state.base import StateViewSet
|
||||||
|
from .view.base import (
|
||||||
GlobalViewViewSet,
|
GlobalViewViewSet,
|
||||||
GlobalViewIssuesViewSet,
|
GlobalViewIssuesViewSet,
|
||||||
IssueViewViewSet,
|
IssueViewViewSet,
|
||||||
IssueViewFavoriteViewSet,
|
IssueViewFavoriteViewSet,
|
||||||
)
|
)
|
||||||
from .cycle import (
|
from .cycle.base import (
|
||||||
CycleViewSet,
|
CycleViewSet,
|
||||||
CycleIssueViewSet,
|
|
||||||
CycleDateCheckEndpoint,
|
CycleDateCheckEndpoint,
|
||||||
CycleFavoriteViewSet,
|
CycleFavoriteViewSet,
|
||||||
TransferCycleIssueEndpoint,
|
TransferCycleIssueEndpoint,
|
||||||
CycleUserPropertiesEndpoint,
|
CycleUserPropertiesEndpoint,
|
||||||
)
|
)
|
||||||
from .asset import FileAssetEndpoint, UserAssetsEndpoint, FileAssetViewSet
|
from .cycle.issue import (
|
||||||
from .issue import (
|
CycleIssueViewSet,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .asset.base import FileAssetEndpoint, UserAssetsEndpoint, FileAssetViewSet
|
||||||
|
from .issue.base import (
|
||||||
IssueListEndpoint,
|
IssueListEndpoint,
|
||||||
IssueViewSet,
|
IssueViewSet,
|
||||||
WorkSpaceIssuesEndpoint,
|
|
||||||
IssueActivityEndpoint,
|
|
||||||
IssueCommentViewSet,
|
|
||||||
IssueUserDisplayPropertyEndpoint,
|
IssueUserDisplayPropertyEndpoint,
|
||||||
LabelViewSet,
|
|
||||||
BulkDeleteIssuesEndpoint,
|
BulkDeleteIssuesEndpoint,
|
||||||
UserWorkSpaceIssues,
|
)
|
||||||
SubIssuesEndpoint,
|
|
||||||
IssueLinkViewSet,
|
from .issue.activity import (
|
||||||
BulkCreateIssueLabelsEndpoint,
|
IssueActivityEndpoint,
|
||||||
IssueAttachmentEndpoint,
|
)
|
||||||
|
|
||||||
|
from .issue.archive import (
|
||||||
IssueArchiveViewSet,
|
IssueArchiveViewSet,
|
||||||
IssueSubscriberViewSet,
|
)
|
||||||
|
|
||||||
|
from .issue.attachment import (
|
||||||
|
IssueAttachmentEndpoint,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .issue.comment import (
|
||||||
|
IssueCommentViewSet,
|
||||||
CommentReactionViewSet,
|
CommentReactionViewSet,
|
||||||
IssueReactionViewSet,
|
)
|
||||||
|
|
||||||
|
from .issue.draft import IssueDraftViewSet
|
||||||
|
|
||||||
|
from .issue.label import (
|
||||||
|
LabelViewSet,
|
||||||
|
BulkCreateIssueLabelsEndpoint,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .issue.link import (
|
||||||
|
IssueLinkViewSet,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .issue.relation import (
|
||||||
IssueRelationViewSet,
|
IssueRelationViewSet,
|
||||||
IssueDraftViewSet,
|
)
|
||||||
|
|
||||||
|
from .issue.reaction import (
|
||||||
|
IssueReactionViewSet,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .issue.sub_issue import (
|
||||||
|
SubIssuesEndpoint,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .issue.subscriber import (
|
||||||
|
IssueSubscriberViewSet,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .auth_extended import (
|
from .auth_extended import (
|
||||||
@ -107,17 +164,21 @@ from .authentication import (
|
|||||||
MagicSignInEndpoint,
|
MagicSignInEndpoint,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .module import (
|
from .module.base import (
|
||||||
ModuleViewSet,
|
ModuleViewSet,
|
||||||
ModuleIssueViewSet,
|
|
||||||
ModuleLinkViewSet,
|
ModuleLinkViewSet,
|
||||||
ModuleFavoriteViewSet,
|
ModuleFavoriteViewSet,
|
||||||
ModuleUserPropertiesEndpoint,
|
ModuleUserPropertiesEndpoint,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from .module.issue import (
|
||||||
|
ModuleIssueViewSet,
|
||||||
|
)
|
||||||
|
|
||||||
from .api import ApiTokenEndpoint
|
from .api import ApiTokenEndpoint
|
||||||
|
|
||||||
from .page import (
|
|
||||||
|
from .page.base import (
|
||||||
PageViewSet,
|
PageViewSet,
|
||||||
PageFavoriteViewSet,
|
PageFavoriteViewSet,
|
||||||
PageLogEndpoint,
|
PageLogEndpoint,
|
||||||
@ -127,19 +188,19 @@ from .page import (
|
|||||||
from .search import GlobalSearchEndpoint, IssueSearchEndpoint
|
from .search import GlobalSearchEndpoint, IssueSearchEndpoint
|
||||||
|
|
||||||
|
|
||||||
from .external import (
|
from .external.base import (
|
||||||
GPTIntegrationEndpoint,
|
GPTIntegrationEndpoint,
|
||||||
UnsplashEndpoint,
|
UnsplashEndpoint,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .estimate import (
|
from .estimate.base import (
|
||||||
ProjectEstimatePointEndpoint,
|
ProjectEstimatePointEndpoint,
|
||||||
BulkEstimatePointEndpoint,
|
BulkEstimatePointEndpoint,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .inbox import InboxViewSet, InboxIssueViewSet
|
from .inbox.base import InboxViewSet, InboxIssueViewSet
|
||||||
|
|
||||||
from .analytic import (
|
from .analytic.base import (
|
||||||
AnalyticsEndpoint,
|
AnalyticsEndpoint,
|
||||||
AnalyticViewViewset,
|
AnalyticViewViewset,
|
||||||
SavedAnalyticEndpoint,
|
SavedAnalyticEndpoint,
|
||||||
@ -147,23 +208,23 @@ from .analytic import (
|
|||||||
DefaultAnalyticsEndpoint,
|
DefaultAnalyticsEndpoint,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .notification import (
|
from .notification.base import (
|
||||||
NotificationViewSet,
|
NotificationViewSet,
|
||||||
UnreadNotificationEndpoint,
|
UnreadNotificationEndpoint,
|
||||||
MarkAllReadNotificationViewSet,
|
MarkAllReadNotificationViewSet,
|
||||||
UserNotificationPreferenceEndpoint,
|
UserNotificationPreferenceEndpoint,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .exporter import ExportIssuesEndpoint
|
from .exporter.base import ExportIssuesEndpoint
|
||||||
|
|
||||||
from .config import ConfigurationEndpoint, MobileConfigurationEndpoint
|
from .config import ConfigurationEndpoint, MobileConfigurationEndpoint
|
||||||
|
|
||||||
from .webhook import (
|
from .webhook.base import (
|
||||||
WebhookEndpoint,
|
WebhookEndpoint,
|
||||||
WebhookLogsEndpoint,
|
WebhookLogsEndpoint,
|
||||||
WebhookSecretRegenerateEndpoint,
|
WebhookSecretRegenerateEndpoint,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .dashboard import DashboardEndpoint, WidgetsEndpoint
|
from .dashboard.base import DashboardEndpoint, WidgetsEndpoint
|
||||||
|
|
||||||
from .error_404 import custom_404_view
|
from .error_404 import custom_404_view
|
||||||
|
@ -4,7 +4,7 @@ from rest_framework.response import Response
|
|||||||
from rest_framework.parsers import MultiPartParser, FormParser, JSONParser
|
from rest_framework.parsers import MultiPartParser, FormParser, JSONParser
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from .base import BaseAPIView, BaseViewSet
|
from ..base import BaseAPIView, BaseViewSet
|
||||||
from plane.db.models import FileAsset, Workspace
|
from plane.db.models import FileAsset, Workspace
|
||||||
from plane.app.serializers import FileAssetSerializer
|
from plane.app.serializers import FileAssetSerializer
|
||||||
|
|
@ -29,7 +29,7 @@ from rest_framework.response import Response
|
|||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from . import BaseViewSet, BaseAPIView, WebhookMixin
|
from .. import BaseViewSet, BaseAPIView, WebhookMixin
|
||||||
from plane.app.serializers import (
|
from plane.app.serializers import (
|
||||||
CycleSerializer,
|
CycleSerializer,
|
||||||
CycleIssueSerializer,
|
CycleIssueSerializer,
|
||||||
@ -660,278 +660,6 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
|
|||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
|
||||||
class CycleIssueViewSet(WebhookMixin, BaseViewSet):
|
|
||||||
serializer_class = CycleIssueSerializer
|
|
||||||
model = CycleIssue
|
|
||||||
|
|
||||||
webhook_event = "cycle_issue"
|
|
||||||
bulk = True
|
|
||||||
|
|
||||||
permission_classes = [
|
|
||||||
ProjectEntityPermission,
|
|
||||||
]
|
|
||||||
|
|
||||||
filterset_fields = [
|
|
||||||
"issue__labels__id",
|
|
||||||
"issue__assignees__id",
|
|
||||||
]
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
return self.filter_queryset(
|
|
||||||
super()
|
|
||||||
.get_queryset()
|
|
||||||
.annotate(
|
|
||||||
sub_issues_count=Issue.issue_objects.filter(
|
|
||||||
parent=OuterRef("issue_id")
|
|
||||||
)
|
|
||||||
.order_by()
|
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
|
||||||
.values("count")
|
|
||||||
)
|
|
||||||
.filter(workspace__slug=self.kwargs.get("slug"))
|
|
||||||
.filter(project_id=self.kwargs.get("project_id"))
|
|
||||||
.filter(
|
|
||||||
project__project_projectmember__member=self.request.user,
|
|
||||||
project__project_projectmember__is_active=True,
|
|
||||||
)
|
|
||||||
.filter(cycle_id=self.kwargs.get("cycle_id"))
|
|
||||||
.select_related("project")
|
|
||||||
.select_related("workspace")
|
|
||||||
.select_related("cycle")
|
|
||||||
.select_related("issue", "issue__state", "issue__project")
|
|
||||||
.prefetch_related("issue__assignees", "issue__labels")
|
|
||||||
.distinct()
|
|
||||||
)
|
|
||||||
|
|
||||||
@method_decorator(gzip_page)
|
|
||||||
def list(self, request, slug, project_id, cycle_id):
|
|
||||||
fields = [
|
|
||||||
field
|
|
||||||
for field in request.GET.get("fields", "").split(",")
|
|
||||||
if field
|
|
||||||
]
|
|
||||||
order_by = request.GET.get("order_by", "created_at")
|
|
||||||
filters = issue_filters(request.query_params, "GET")
|
|
||||||
queryset = (
|
|
||||||
Issue.issue_objects.filter(issue_cycle__cycle_id=cycle_id)
|
|
||||||
.filter(project_id=project_id)
|
|
||||||
.filter(workspace__slug=slug)
|
|
||||||
.filter(**filters)
|
|
||||||
.select_related("workspace", "project", "state", "parent")
|
|
||||||
.prefetch_related(
|
|
||||||
"assignees",
|
|
||||||
"labels",
|
|
||||||
"issue_module__module",
|
|
||||||
"issue_cycle__cycle",
|
|
||||||
)
|
|
||||||
.order_by(order_by)
|
|
||||||
.filter(**filters)
|
|
||||||
.annotate(cycle_id=F("issue_cycle__cycle_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")
|
|
||||||
)
|
|
||||||
.annotate(
|
|
||||||
sub_issues_count=Issue.issue_objects.filter(
|
|
||||||
parent=OuterRef("id")
|
|
||||||
)
|
|
||||||
.order_by()
|
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
|
||||||
.values("count")
|
|
||||||
)
|
|
||||||
.annotate(
|
|
||||||
label_ids=Coalesce(
|
|
||||||
ArrayAgg(
|
|
||||||
"labels__id",
|
|
||||||
distinct=True,
|
|
||||||
filter=~Q(labels__id__isnull=True),
|
|
||||||
),
|
|
||||||
Value([], output_field=ArrayField(UUIDField())),
|
|
||||||
),
|
|
||||||
assignee_ids=Coalesce(
|
|
||||||
ArrayAgg(
|
|
||||||
"assignees__id",
|
|
||||||
distinct=True,
|
|
||||||
filter=~Q(assignees__id__isnull=True),
|
|
||||||
),
|
|
||||||
Value([], output_field=ArrayField(UUIDField())),
|
|
||||||
),
|
|
||||||
module_ids=Coalesce(
|
|
||||||
ArrayAgg(
|
|
||||||
"issue_module__module_id",
|
|
||||||
distinct=True,
|
|
||||||
filter=~Q(issue_module__module_id__isnull=True),
|
|
||||||
),
|
|
||||||
Value([], output_field=ArrayField(UUIDField())),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.order_by(order_by)
|
|
||||||
)
|
|
||||||
if self.fields:
|
|
||||||
issues = IssueSerializer(
|
|
||||||
queryset, many=True, fields=fields if fields else None
|
|
||||||
).data
|
|
||||||
else:
|
|
||||||
issues = queryset.values(
|
|
||||||
"id",
|
|
||||||
"name",
|
|
||||||
"state_id",
|
|
||||||
"sort_order",
|
|
||||||
"completed_at",
|
|
||||||
"estimate_point",
|
|
||||||
"priority",
|
|
||||||
"start_date",
|
|
||||||
"target_date",
|
|
||||||
"sequence_id",
|
|
||||||
"project_id",
|
|
||||||
"parent_id",
|
|
||||||
"cycle_id",
|
|
||||||
"module_ids",
|
|
||||||
"label_ids",
|
|
||||||
"assignee_ids",
|
|
||||||
"sub_issues_count",
|
|
||||||
"created_at",
|
|
||||||
"updated_at",
|
|
||||||
"created_by",
|
|
||||||
"updated_by",
|
|
||||||
"attachment_count",
|
|
||||||
"link_count",
|
|
||||||
"is_draft",
|
|
||||||
"archived_at",
|
|
||||||
)
|
|
||||||
return Response(issues, status=status.HTTP_200_OK)
|
|
||||||
|
|
||||||
def create(self, request, slug, project_id, cycle_id):
|
|
||||||
issues = request.data.get("issues", [])
|
|
||||||
|
|
||||||
if not issues:
|
|
||||||
return Response(
|
|
||||||
{"error": "Issues are required"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
cycle = Cycle.objects.get(
|
|
||||||
workspace__slug=slug, project_id=project_id, pk=cycle_id
|
|
||||||
)
|
|
||||||
|
|
||||||
if (
|
|
||||||
cycle.end_date is not None
|
|
||||||
and cycle.end_date < timezone.now().date()
|
|
||||||
):
|
|
||||||
return Response(
|
|
||||||
{
|
|
||||||
"error": "The Cycle has already been completed so no new issues can be added"
|
|
||||||
},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Get all CycleIssues already created
|
|
||||||
cycle_issues = list(
|
|
||||||
CycleIssue.objects.filter(
|
|
||||||
~Q(cycle_id=cycle_id), issue_id__in=issues
|
|
||||||
)
|
|
||||||
)
|
|
||||||
existing_issues = [
|
|
||||||
str(cycle_issue.issue_id) for cycle_issue in cycle_issues
|
|
||||||
]
|
|
||||||
new_issues = list(set(issues) - set(existing_issues))
|
|
||||||
|
|
||||||
# New issues to create
|
|
||||||
created_records = CycleIssue.objects.bulk_create(
|
|
||||||
[
|
|
||||||
CycleIssue(
|
|
||||||
project_id=project_id,
|
|
||||||
workspace_id=cycle.workspace_id,
|
|
||||||
created_by_id=request.user.id,
|
|
||||||
updated_by_id=request.user.id,
|
|
||||||
cycle_id=cycle_id,
|
|
||||||
issue_id=issue,
|
|
||||||
)
|
|
||||||
for issue in new_issues
|
|
||||||
],
|
|
||||||
batch_size=10,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Updated Issues
|
|
||||||
updated_records = []
|
|
||||||
update_cycle_issue_activity = []
|
|
||||||
# Iterate over each cycle_issue in cycle_issues
|
|
||||||
for cycle_issue in cycle_issues:
|
|
||||||
# Update the cycle_issue's cycle_id
|
|
||||||
cycle_issue.cycle_id = cycle_id
|
|
||||||
# Add the modified cycle_issue to the records_to_update list
|
|
||||||
updated_records.append(cycle_issue)
|
|
||||||
# Record the update activity
|
|
||||||
update_cycle_issue_activity.append(
|
|
||||||
{
|
|
||||||
"old_cycle_id": str(cycle_issue.cycle_id),
|
|
||||||
"new_cycle_id": str(cycle_id),
|
|
||||||
"issue_id": str(cycle_issue.issue_id),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Update the cycle issues
|
|
||||||
CycleIssue.objects.bulk_update(
|
|
||||||
updated_records, ["cycle_id"], batch_size=100
|
|
||||||
)
|
|
||||||
# Capture Issue Activity
|
|
||||||
issue_activity.delay(
|
|
||||||
type="cycle.activity.created",
|
|
||||||
requested_data=json.dumps({"cycles_list": issues}),
|
|
||||||
actor_id=str(self.request.user.id),
|
|
||||||
issue_id=None,
|
|
||||||
project_id=str(self.kwargs.get("project_id", None)),
|
|
||||||
current_instance=json.dumps(
|
|
||||||
{
|
|
||||||
"updated_cycle_issues": update_cycle_issue_activity,
|
|
||||||
"created_cycle_issues": serializers.serialize(
|
|
||||||
"json", created_records
|
|
||||||
),
|
|
||||||
}
|
|
||||||
),
|
|
||||||
epoch=int(timezone.now().timestamp()),
|
|
||||||
notification=True,
|
|
||||||
origin=request.META.get("HTTP_ORIGIN"),
|
|
||||||
)
|
|
||||||
return Response({"message": "success"}, status=status.HTTP_201_CREATED)
|
|
||||||
|
|
||||||
def destroy(self, request, slug, project_id, cycle_id, issue_id):
|
|
||||||
cycle_issue = CycleIssue.objects.get(
|
|
||||||
issue_id=issue_id,
|
|
||||||
workspace__slug=slug,
|
|
||||||
project_id=project_id,
|
|
||||||
cycle_id=cycle_id,
|
|
||||||
)
|
|
||||||
issue_activity.delay(
|
|
||||||
type="cycle.activity.deleted",
|
|
||||||
requested_data=json.dumps(
|
|
||||||
{
|
|
||||||
"cycle_id": str(self.kwargs.get("cycle_id")),
|
|
||||||
"issues": [str(issue_id)],
|
|
||||||
}
|
|
||||||
),
|
|
||||||
actor_id=str(self.request.user.id),
|
|
||||||
issue_id=str(issue_id),
|
|
||||||
project_id=str(self.kwargs.get("project_id", None)),
|
|
||||||
current_instance=None,
|
|
||||||
epoch=int(timezone.now().timestamp()),
|
|
||||||
notification=True,
|
|
||||||
origin=request.META.get("HTTP_ORIGIN"),
|
|
||||||
)
|
|
||||||
cycle_issue.delete()
|
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
|
||||||
|
|
||||||
|
|
||||||
class CycleDateCheckEndpoint(BaseAPIView):
|
class CycleDateCheckEndpoint(BaseAPIView):
|
||||||
permission_classes = [
|
permission_classes = [
|
||||||
ProjectEntityPermission,
|
ProjectEntityPermission,
|
312
apiserver/plane/app/views/cycle/issue.py
Normal file
312
apiserver/plane/app/views/cycle/issue.py
Normal file
@ -0,0 +1,312 @@
|
|||||||
|
# Python imports
|
||||||
|
import json
|
||||||
|
|
||||||
|
# Django imports
|
||||||
|
from django.db.models import (
|
||||||
|
Func,
|
||||||
|
F,
|
||||||
|
Q,
|
||||||
|
OuterRef,
|
||||||
|
Value,
|
||||||
|
UUIDField,
|
||||||
|
)
|
||||||
|
from django.core import serializers
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
|
from django.views.decorators.gzip import gzip_page
|
||||||
|
from django.contrib.postgres.aggregates import ArrayAgg
|
||||||
|
from django.contrib.postgres.fields import ArrayField
|
||||||
|
from django.db.models.functions import Coalesce
|
||||||
|
|
||||||
|
# Third party imports
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework import status
|
||||||
|
|
||||||
|
# Module imports
|
||||||
|
from .. import BaseViewSet, WebhookMixin
|
||||||
|
from plane.app.serializers import (
|
||||||
|
IssueSerializer,
|
||||||
|
CycleIssueSerializer,
|
||||||
|
)
|
||||||
|
from plane.app.permissions import ProjectEntityPermission
|
||||||
|
from plane.db.models import (
|
||||||
|
Cycle,
|
||||||
|
CycleIssue,
|
||||||
|
Issue,
|
||||||
|
IssueLink,
|
||||||
|
IssueAttachment,
|
||||||
|
)
|
||||||
|
from plane.bgtasks.issue_activites_task import issue_activity
|
||||||
|
from plane.utils.issue_filters import issue_filters
|
||||||
|
|
||||||
|
|
||||||
|
class CycleIssueViewSet(WebhookMixin, BaseViewSet):
|
||||||
|
serializer_class = CycleIssueSerializer
|
||||||
|
model = CycleIssue
|
||||||
|
|
||||||
|
webhook_event = "cycle_issue"
|
||||||
|
bulk = True
|
||||||
|
|
||||||
|
permission_classes = [
|
||||||
|
ProjectEntityPermission,
|
||||||
|
]
|
||||||
|
|
||||||
|
filterset_fields = [
|
||||||
|
"issue__labels__id",
|
||||||
|
"issue__assignees__id",
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return self.filter_queryset(
|
||||||
|
super()
|
||||||
|
.get_queryset()
|
||||||
|
.annotate(
|
||||||
|
sub_issues_count=Issue.issue_objects.filter(
|
||||||
|
parent=OuterRef("issue_id")
|
||||||
|
)
|
||||||
|
.order_by()
|
||||||
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
|
.values("count")
|
||||||
|
)
|
||||||
|
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||||
|
.filter(project_id=self.kwargs.get("project_id"))
|
||||||
|
.filter(
|
||||||
|
project__project_projectmember__member=self.request.user,
|
||||||
|
project__project_projectmember__is_active=True,
|
||||||
|
)
|
||||||
|
.filter(cycle_id=self.kwargs.get("cycle_id"))
|
||||||
|
.select_related("project")
|
||||||
|
.select_related("workspace")
|
||||||
|
.select_related("cycle")
|
||||||
|
.select_related("issue", "issue__state", "issue__project")
|
||||||
|
.prefetch_related("issue__assignees", "issue__labels")
|
||||||
|
.distinct()
|
||||||
|
)
|
||||||
|
|
||||||
|
@method_decorator(gzip_page)
|
||||||
|
def list(self, request, slug, project_id, cycle_id):
|
||||||
|
fields = [
|
||||||
|
field
|
||||||
|
for field in request.GET.get("fields", "").split(",")
|
||||||
|
if field
|
||||||
|
]
|
||||||
|
order_by = request.GET.get("order_by", "created_at")
|
||||||
|
filters = issue_filters(request.query_params, "GET")
|
||||||
|
queryset = (
|
||||||
|
Issue.issue_objects.filter(issue_cycle__cycle_id=cycle_id)
|
||||||
|
.filter(project_id=project_id)
|
||||||
|
.filter(workspace__slug=slug)
|
||||||
|
.filter(**filters)
|
||||||
|
.select_related("workspace", "project", "state", "parent")
|
||||||
|
.prefetch_related(
|
||||||
|
"assignees",
|
||||||
|
"labels",
|
||||||
|
"issue_module__module",
|
||||||
|
"issue_cycle__cycle",
|
||||||
|
)
|
||||||
|
.order_by(order_by)
|
||||||
|
.filter(**filters)
|
||||||
|
.annotate(cycle_id=F("issue_cycle__cycle_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")
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
sub_issues_count=Issue.issue_objects.filter(
|
||||||
|
parent=OuterRef("id")
|
||||||
|
)
|
||||||
|
.order_by()
|
||||||
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
|
.values("count")
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
label_ids=Coalesce(
|
||||||
|
ArrayAgg(
|
||||||
|
"labels__id",
|
||||||
|
distinct=True,
|
||||||
|
filter=~Q(labels__id__isnull=True),
|
||||||
|
),
|
||||||
|
Value([], output_field=ArrayField(UUIDField())),
|
||||||
|
),
|
||||||
|
assignee_ids=Coalesce(
|
||||||
|
ArrayAgg(
|
||||||
|
"assignees__id",
|
||||||
|
distinct=True,
|
||||||
|
filter=~Q(assignees__id__isnull=True),
|
||||||
|
),
|
||||||
|
Value([], output_field=ArrayField(UUIDField())),
|
||||||
|
),
|
||||||
|
module_ids=Coalesce(
|
||||||
|
ArrayAgg(
|
||||||
|
"issue_module__module_id",
|
||||||
|
distinct=True,
|
||||||
|
filter=~Q(issue_module__module_id__isnull=True),
|
||||||
|
),
|
||||||
|
Value([], output_field=ArrayField(UUIDField())),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.order_by(order_by)
|
||||||
|
)
|
||||||
|
if self.fields:
|
||||||
|
issues = IssueSerializer(
|
||||||
|
queryset, many=True, fields=fields if fields else None
|
||||||
|
).data
|
||||||
|
else:
|
||||||
|
issues = queryset.values(
|
||||||
|
"id",
|
||||||
|
"name",
|
||||||
|
"state_id",
|
||||||
|
"sort_order",
|
||||||
|
"completed_at",
|
||||||
|
"estimate_point",
|
||||||
|
"priority",
|
||||||
|
"start_date",
|
||||||
|
"target_date",
|
||||||
|
"sequence_id",
|
||||||
|
"project_id",
|
||||||
|
"parent_id",
|
||||||
|
"cycle_id",
|
||||||
|
"module_ids",
|
||||||
|
"label_ids",
|
||||||
|
"assignee_ids",
|
||||||
|
"sub_issues_count",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
"created_by",
|
||||||
|
"updated_by",
|
||||||
|
"attachment_count",
|
||||||
|
"link_count",
|
||||||
|
"is_draft",
|
||||||
|
"archived_at",
|
||||||
|
)
|
||||||
|
return Response(issues, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
def create(self, request, slug, project_id, cycle_id):
|
||||||
|
issues = request.data.get("issues", [])
|
||||||
|
|
||||||
|
if not issues:
|
||||||
|
return Response(
|
||||||
|
{"error": "Issues are required"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
cycle = Cycle.objects.get(
|
||||||
|
workspace__slug=slug, project_id=project_id, pk=cycle_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
cycle.end_date is not None
|
||||||
|
and cycle.end_date < timezone.now().date()
|
||||||
|
):
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"error": "The Cycle has already been completed so no new issues can be added"
|
||||||
|
},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get all CycleIssues already created
|
||||||
|
cycle_issues = list(
|
||||||
|
CycleIssue.objects.filter(
|
||||||
|
~Q(cycle_id=cycle_id), issue_id__in=issues
|
||||||
|
)
|
||||||
|
)
|
||||||
|
existing_issues = [
|
||||||
|
str(cycle_issue.issue_id) for cycle_issue in cycle_issues
|
||||||
|
]
|
||||||
|
new_issues = list(set(issues) - set(existing_issues))
|
||||||
|
|
||||||
|
# New issues to create
|
||||||
|
created_records = CycleIssue.objects.bulk_create(
|
||||||
|
[
|
||||||
|
CycleIssue(
|
||||||
|
project_id=project_id,
|
||||||
|
workspace_id=cycle.workspace_id,
|
||||||
|
created_by_id=request.user.id,
|
||||||
|
updated_by_id=request.user.id,
|
||||||
|
cycle_id=cycle_id,
|
||||||
|
issue_id=issue,
|
||||||
|
)
|
||||||
|
for issue in new_issues
|
||||||
|
],
|
||||||
|
batch_size=10,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Updated Issues
|
||||||
|
updated_records = []
|
||||||
|
update_cycle_issue_activity = []
|
||||||
|
# Iterate over each cycle_issue in cycle_issues
|
||||||
|
for cycle_issue in cycle_issues:
|
||||||
|
# Update the cycle_issue's cycle_id
|
||||||
|
cycle_issue.cycle_id = cycle_id
|
||||||
|
# Add the modified cycle_issue to the records_to_update list
|
||||||
|
updated_records.append(cycle_issue)
|
||||||
|
# Record the update activity
|
||||||
|
update_cycle_issue_activity.append(
|
||||||
|
{
|
||||||
|
"old_cycle_id": str(cycle_issue.cycle_id),
|
||||||
|
"new_cycle_id": str(cycle_id),
|
||||||
|
"issue_id": str(cycle_issue.issue_id),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update the cycle issues
|
||||||
|
CycleIssue.objects.bulk_update(
|
||||||
|
updated_records, ["cycle_id"], batch_size=100
|
||||||
|
)
|
||||||
|
# Capture Issue Activity
|
||||||
|
issue_activity.delay(
|
||||||
|
type="cycle.activity.created",
|
||||||
|
requested_data=json.dumps({"cycles_list": issues}),
|
||||||
|
actor_id=str(self.request.user.id),
|
||||||
|
issue_id=None,
|
||||||
|
project_id=str(self.kwargs.get("project_id", None)),
|
||||||
|
current_instance=json.dumps(
|
||||||
|
{
|
||||||
|
"updated_cycle_issues": update_cycle_issue_activity,
|
||||||
|
"created_cycle_issues": serializers.serialize(
|
||||||
|
"json", created_records
|
||||||
|
),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
epoch=int(timezone.now().timestamp()),
|
||||||
|
notification=True,
|
||||||
|
origin=request.META.get("HTTP_ORIGIN"),
|
||||||
|
)
|
||||||
|
return Response({"message": "success"}, status=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
|
def destroy(self, request, slug, project_id, cycle_id, issue_id):
|
||||||
|
cycle_issue = CycleIssue.objects.get(
|
||||||
|
issue_id=issue_id,
|
||||||
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
cycle_id=cycle_id,
|
||||||
|
)
|
||||||
|
issue_activity.delay(
|
||||||
|
type="cycle.activity.deleted",
|
||||||
|
requested_data=json.dumps(
|
||||||
|
{
|
||||||
|
"cycle_id": str(self.kwargs.get("cycle_id")),
|
||||||
|
"issues": [str(issue_id)],
|
||||||
|
}
|
||||||
|
),
|
||||||
|
actor_id=str(self.request.user.id),
|
||||||
|
issue_id=str(issue_id),
|
||||||
|
project_id=str(self.kwargs.get("project_id", None)),
|
||||||
|
current_instance=None,
|
||||||
|
epoch=int(timezone.now().timestamp()),
|
||||||
|
notification=True,
|
||||||
|
origin=request.META.get("HTTP_ORIGIN"),
|
||||||
|
)
|
||||||
|
cycle_issue.delete()
|
||||||
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
@ -26,7 +26,7 @@ from rest_framework.response import Response
|
|||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from . import BaseAPIView
|
from .. import BaseAPIView
|
||||||
from plane.db.models import (
|
from plane.db.models import (
|
||||||
Issue,
|
Issue,
|
||||||
IssueActivity,
|
IssueActivity,
|
@ -3,7 +3,7 @@ from rest_framework.response import Response
|
|||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from .base import BaseViewSet, BaseAPIView
|
from ..base import BaseViewSet, BaseAPIView
|
||||||
from plane.app.permissions import ProjectEntityPermission
|
from plane.app.permissions import ProjectEntityPermission
|
||||||
from plane.db.models import Project, Estimate, EstimatePoint
|
from plane.db.models import Project, Estimate, EstimatePoint
|
||||||
from plane.app.serializers import (
|
from plane.app.serializers import (
|
@ -3,7 +3,7 @@ from rest_framework.response import Response
|
|||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from . import BaseAPIView
|
from .. import BaseAPIView
|
||||||
from plane.app.permissions import WorkSpaceAdminPermission
|
from plane.app.permissions import WorkSpaceAdminPermission
|
||||||
from plane.bgtasks.export_task import issue_export_task
|
from plane.bgtasks.export_task import issue_export_task
|
||||||
from plane.db.models import Project, ExporterHistory, Workspace
|
from plane.db.models import Project, ExporterHistory, Workspace
|
@ -10,7 +10,7 @@ from rest_framework import status
|
|||||||
# Django imports
|
# Django imports
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from .base import BaseAPIView
|
from ..base import BaseAPIView
|
||||||
from plane.app.permissions import ProjectEntityPermission
|
from plane.app.permissions import ProjectEntityPermission
|
||||||
from plane.db.models import Workspace, Project
|
from plane.db.models import Workspace, Project
|
||||||
from plane.app.serializers import (
|
from plane.app.serializers import (
|
@ -15,7 +15,7 @@ from rest_framework import status
|
|||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from .base import BaseViewSet
|
from ..base import BaseViewSet
|
||||||
from plane.app.permissions import ProjectBasePermission, ProjectLitePermission
|
from plane.app.permissions import ProjectBasePermission, ProjectLitePermission
|
||||||
from plane.db.models import (
|
from plane.db.models import (
|
||||||
Inbox,
|
Inbox,
|
File diff suppressed because it is too large
Load Diff
85
apiserver/plane/app/views/issue/activity.py
Normal file
85
apiserver/plane/app/views/issue/activity.py
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
# Python imports
|
||||||
|
from itertools import chain
|
||||||
|
|
||||||
|
# Django imports
|
||||||
|
from django.db.models import (
|
||||||
|
Prefetch,
|
||||||
|
Q,
|
||||||
|
)
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
|
from django.views.decorators.gzip import gzip_page
|
||||||
|
|
||||||
|
# Third Party imports
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework import status
|
||||||
|
|
||||||
|
# Module imports
|
||||||
|
from .. import BaseAPIView
|
||||||
|
from plane.app.serializers import (
|
||||||
|
IssueActivitySerializer,
|
||||||
|
IssueCommentSerializer,
|
||||||
|
)
|
||||||
|
from plane.app.permissions import ProjectEntityPermission
|
||||||
|
from plane.db.models import (
|
||||||
|
IssueActivity,
|
||||||
|
IssueComment,
|
||||||
|
CommentReaction,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class IssueActivityEndpoint(BaseAPIView):
|
||||||
|
permission_classes = [
|
||||||
|
ProjectEntityPermission,
|
||||||
|
]
|
||||||
|
|
||||||
|
@method_decorator(gzip_page)
|
||||||
|
def get(self, request, slug, project_id, issue_id):
|
||||||
|
filters = {}
|
||||||
|
if request.GET.get("created_at__gt", None) is not None:
|
||||||
|
filters = {"created_at__gt": request.GET.get("created_at__gt")}
|
||||||
|
|
||||||
|
issue_activities = (
|
||||||
|
IssueActivity.objects.filter(issue_id=issue_id)
|
||||||
|
.filter(
|
||||||
|
~Q(field__in=["comment", "vote", "reaction", "draft"]),
|
||||||
|
project__project_projectmember__member=self.request.user,
|
||||||
|
project__project_projectmember__is_active=True,
|
||||||
|
workspace__slug=slug,
|
||||||
|
)
|
||||||
|
.filter(**filters)
|
||||||
|
.select_related("actor", "workspace", "issue", "project")
|
||||||
|
).order_by("created_at")
|
||||||
|
issue_comments = (
|
||||||
|
IssueComment.objects.filter(issue_id=issue_id)
|
||||||
|
.filter(
|
||||||
|
project__project_projectmember__member=self.request.user,
|
||||||
|
project__project_projectmember__is_active=True,
|
||||||
|
workspace__slug=slug,
|
||||||
|
)
|
||||||
|
.filter(**filters)
|
||||||
|
.order_by("created_at")
|
||||||
|
.select_related("actor", "issue", "project", "workspace")
|
||||||
|
.prefetch_related(
|
||||||
|
Prefetch(
|
||||||
|
"comment_reactions",
|
||||||
|
queryset=CommentReaction.objects.select_related("actor"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
issue_activities = IssueActivitySerializer(
|
||||||
|
issue_activities, many=True
|
||||||
|
).data
|
||||||
|
issue_comments = IssueCommentSerializer(issue_comments, many=True).data
|
||||||
|
|
||||||
|
if request.GET.get("activity_type", None) == "issue-property":
|
||||||
|
return Response(issue_activities, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
if request.GET.get("activity_type", None) == "issue-comment":
|
||||||
|
return Response(issue_comments, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
result_list = sorted(
|
||||||
|
chain(issue_activities, issue_comments),
|
||||||
|
key=lambda instance: instance["created_at"],
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response(result_list, status=status.HTTP_200_OK)
|
347
apiserver/plane/app/views/issue/archive.py
Normal file
347
apiserver/plane/app/views/issue/archive.py
Normal file
@ -0,0 +1,347 @@
|
|||||||
|
# Python imports
|
||||||
|
import json
|
||||||
|
|
||||||
|
# Django imports
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.db.models import (
|
||||||
|
Prefetch,
|
||||||
|
OuterRef,
|
||||||
|
Func,
|
||||||
|
F,
|
||||||
|
Q,
|
||||||
|
Case,
|
||||||
|
Value,
|
||||||
|
CharField,
|
||||||
|
When,
|
||||||
|
Exists,
|
||||||
|
Max,
|
||||||
|
UUIDField,
|
||||||
|
)
|
||||||
|
from django.core.serializers.json import DjangoJSONEncoder
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
|
from django.views.decorators.gzip import gzip_page
|
||||||
|
from django.contrib.postgres.aggregates import ArrayAgg
|
||||||
|
from django.contrib.postgres.fields import ArrayField
|
||||||
|
from django.db.models.functions import Coalesce
|
||||||
|
|
||||||
|
# Third Party imports
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework import status
|
||||||
|
|
||||||
|
# Module imports
|
||||||
|
from .. import BaseViewSet
|
||||||
|
from plane.app.serializers import (
|
||||||
|
IssueSerializer,
|
||||||
|
IssueFlatSerializer,
|
||||||
|
IssueDetailSerializer,
|
||||||
|
)
|
||||||
|
from plane.app.permissions import (
|
||||||
|
ProjectEntityPermission,
|
||||||
|
)
|
||||||
|
from plane.db.models import (
|
||||||
|
Issue,
|
||||||
|
IssueLink,
|
||||||
|
IssueAttachment,
|
||||||
|
IssueSubscriber,
|
||||||
|
IssueReaction,
|
||||||
|
)
|
||||||
|
from plane.bgtasks.issue_activites_task import issue_activity
|
||||||
|
from plane.utils.issue_filters import issue_filters
|
||||||
|
|
||||||
|
|
||||||
|
class IssueArchiveViewSet(BaseViewSet):
|
||||||
|
permission_classes = [
|
||||||
|
ProjectEntityPermission,
|
||||||
|
]
|
||||||
|
serializer_class = IssueFlatSerializer
|
||||||
|
model = Issue
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return (
|
||||||
|
Issue.objects.annotate(
|
||||||
|
sub_issues_count=Issue.objects.filter(parent=OuterRef("id"))
|
||||||
|
.order_by()
|
||||||
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
|
.values("count")
|
||||||
|
)
|
||||||
|
.filter(archived_at__isnull=False)
|
||||||
|
.filter(project_id=self.kwargs.get("project_id"))
|
||||||
|
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||||
|
.select_related("workspace", "project", "state", "parent")
|
||||||
|
.prefetch_related("assignees", "labels", "issue_module__module")
|
||||||
|
.annotate(cycle_id=F("issue_cycle__cycle_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")
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
sub_issues_count=Issue.issue_objects.filter(
|
||||||
|
parent=OuterRef("id")
|
||||||
|
)
|
||||||
|
.order_by()
|
||||||
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
|
.values("count")
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
label_ids=Coalesce(
|
||||||
|
ArrayAgg(
|
||||||
|
"labels__id",
|
||||||
|
distinct=True,
|
||||||
|
filter=~Q(labels__id__isnull=True),
|
||||||
|
),
|
||||||
|
Value([], output_field=ArrayField(UUIDField())),
|
||||||
|
),
|
||||||
|
assignee_ids=Coalesce(
|
||||||
|
ArrayAgg(
|
||||||
|
"assignees__id",
|
||||||
|
distinct=True,
|
||||||
|
filter=~Q(assignees__id__isnull=True),
|
||||||
|
),
|
||||||
|
Value([], output_field=ArrayField(UUIDField())),
|
||||||
|
),
|
||||||
|
module_ids=Coalesce(
|
||||||
|
ArrayAgg(
|
||||||
|
"issue_module__module_id",
|
||||||
|
distinct=True,
|
||||||
|
filter=~Q(issue_module__module_id__isnull=True),
|
||||||
|
),
|
||||||
|
Value([], output_field=ArrayField(UUIDField())),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
@method_decorator(gzip_page)
|
||||||
|
def list(self, request, slug, project_id):
|
||||||
|
filters = issue_filters(request.query_params, "GET")
|
||||||
|
show_sub_issues = request.GET.get("show_sub_issues", "true")
|
||||||
|
|
||||||
|
# Custom ordering for priority and state
|
||||||
|
priority_order = ["urgent", "high", "medium", "low", "none"]
|
||||||
|
state_order = [
|
||||||
|
"backlog",
|
||||||
|
"unstarted",
|
||||||
|
"started",
|
||||||
|
"completed",
|
||||||
|
"cancelled",
|
||||||
|
]
|
||||||
|
|
||||||
|
order_by_param = request.GET.get("order_by", "-created_at")
|
||||||
|
|
||||||
|
issue_queryset = self.get_queryset().filter(**filters)
|
||||||
|
|
||||||
|
# Priority Ordering
|
||||||
|
if order_by_param == "priority" or order_by_param == "-priority":
|
||||||
|
priority_order = (
|
||||||
|
priority_order
|
||||||
|
if order_by_param == "priority"
|
||||||
|
else priority_order[::-1]
|
||||||
|
)
|
||||||
|
issue_queryset = issue_queryset.annotate(
|
||||||
|
priority_order=Case(
|
||||||
|
*[
|
||||||
|
When(priority=p, then=Value(i))
|
||||||
|
for i, p in enumerate(priority_order)
|
||||||
|
],
|
||||||
|
output_field=CharField(),
|
||||||
|
)
|
||||||
|
).order_by("priority_order")
|
||||||
|
|
||||||
|
# State Ordering
|
||||||
|
elif order_by_param in [
|
||||||
|
"state__name",
|
||||||
|
"state__group",
|
||||||
|
"-state__name",
|
||||||
|
"-state__group",
|
||||||
|
]:
|
||||||
|
state_order = (
|
||||||
|
state_order
|
||||||
|
if order_by_param in ["state__name", "state__group"]
|
||||||
|
else state_order[::-1]
|
||||||
|
)
|
||||||
|
issue_queryset = issue_queryset.annotate(
|
||||||
|
state_order=Case(
|
||||||
|
*[
|
||||||
|
When(state__group=state_group, then=Value(i))
|
||||||
|
for i, state_group in enumerate(state_order)
|
||||||
|
],
|
||||||
|
default=Value(len(state_order)),
|
||||||
|
output_field=CharField(),
|
||||||
|
)
|
||||||
|
).order_by("state_order")
|
||||||
|
# assignee and label ordering
|
||||||
|
elif order_by_param in [
|
||||||
|
"labels__name",
|
||||||
|
"-labels__name",
|
||||||
|
"assignees__first_name",
|
||||||
|
"-assignees__first_name",
|
||||||
|
]:
|
||||||
|
issue_queryset = issue_queryset.annotate(
|
||||||
|
max_values=Max(
|
||||||
|
order_by_param[1::]
|
||||||
|
if order_by_param.startswith("-")
|
||||||
|
else order_by_param
|
||||||
|
)
|
||||||
|
).order_by(
|
||||||
|
"-max_values"
|
||||||
|
if order_by_param.startswith("-")
|
||||||
|
else "max_values"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
issue_queryset = issue_queryset.order_by(order_by_param)
|
||||||
|
|
||||||
|
issue_queryset = (
|
||||||
|
issue_queryset
|
||||||
|
if show_sub_issues == "true"
|
||||||
|
else issue_queryset.filter(parent__isnull=True)
|
||||||
|
)
|
||||||
|
if self.expand or self.fields:
|
||||||
|
issues = IssueSerializer(
|
||||||
|
issue_queryset,
|
||||||
|
many=True,
|
||||||
|
fields=self.fields,
|
||||||
|
).data
|
||||||
|
else:
|
||||||
|
issues = issue_queryset.values(
|
||||||
|
"id",
|
||||||
|
"name",
|
||||||
|
"state_id",
|
||||||
|
"sort_order",
|
||||||
|
"completed_at",
|
||||||
|
"estimate_point",
|
||||||
|
"priority",
|
||||||
|
"start_date",
|
||||||
|
"target_date",
|
||||||
|
"sequence_id",
|
||||||
|
"project_id",
|
||||||
|
"parent_id",
|
||||||
|
"cycle_id",
|
||||||
|
"module_ids",
|
||||||
|
"label_ids",
|
||||||
|
"assignee_ids",
|
||||||
|
"sub_issues_count",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
"created_by",
|
||||||
|
"updated_by",
|
||||||
|
"attachment_count",
|
||||||
|
"link_count",
|
||||||
|
"is_draft",
|
||||||
|
"archived_at",
|
||||||
|
)
|
||||||
|
return Response(issues, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
def retrieve(self, request, slug, project_id, pk=None):
|
||||||
|
issue = (
|
||||||
|
self.get_queryset()
|
||||||
|
.filter(pk=pk)
|
||||||
|
.prefetch_related(
|
||||||
|
Prefetch(
|
||||||
|
"issue_reactions",
|
||||||
|
queryset=IssueReaction.objects.select_related(
|
||||||
|
"issue", "actor"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.prefetch_related(
|
||||||
|
Prefetch(
|
||||||
|
"issue_attachment",
|
||||||
|
queryset=IssueAttachment.objects.select_related("issue"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.prefetch_related(
|
||||||
|
Prefetch(
|
||||||
|
"issue_link",
|
||||||
|
queryset=IssueLink.objects.select_related("created_by"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
is_subscribed=Exists(
|
||||||
|
IssueSubscriber.objects.filter(
|
||||||
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
issue_id=OuterRef("pk"),
|
||||||
|
subscriber=request.user,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
).first()
|
||||||
|
if not issue:
|
||||||
|
return Response(
|
||||||
|
{"error": "The required object does not exist."},
|
||||||
|
status=status.HTTP_404_NOT_FOUND,
|
||||||
|
)
|
||||||
|
serializer = IssueDetailSerializer(issue, expand=self.expand)
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
def archive(self, request, slug, project_id, pk=None):
|
||||||
|
issue = Issue.issue_objects.get(
|
||||||
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
pk=pk,
|
||||||
|
)
|
||||||
|
if issue.state.group not in ["completed", "cancelled"]:
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"error": "Can only archive completed or cancelled state group issue"
|
||||||
|
},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
issue_activity.delay(
|
||||||
|
type="issue.activity.updated",
|
||||||
|
requested_data=json.dumps(
|
||||||
|
{
|
||||||
|
"archived_at": str(timezone.now().date()),
|
||||||
|
"automation": False,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
actor_id=str(request.user.id),
|
||||||
|
issue_id=str(issue.id),
|
||||||
|
project_id=str(project_id),
|
||||||
|
current_instance=json.dumps(
|
||||||
|
IssueSerializer(issue).data, cls=DjangoJSONEncoder
|
||||||
|
),
|
||||||
|
epoch=int(timezone.now().timestamp()),
|
||||||
|
notification=True,
|
||||||
|
origin=request.META.get("HTTP_ORIGIN"),
|
||||||
|
)
|
||||||
|
issue.archived_at = timezone.now().date()
|
||||||
|
issue.save()
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
{"archived_at": str(issue.archived_at)}, status=status.HTTP_200_OK
|
||||||
|
)
|
||||||
|
|
||||||
|
def unarchive(self, request, slug, project_id, pk=None):
|
||||||
|
issue = Issue.objects.get(
|
||||||
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
archived_at__isnull=False,
|
||||||
|
pk=pk,
|
||||||
|
)
|
||||||
|
issue_activity.delay(
|
||||||
|
type="issue.activity.updated",
|
||||||
|
requested_data=json.dumps({"archived_at": None}),
|
||||||
|
actor_id=str(request.user.id),
|
||||||
|
issue_id=str(issue.id),
|
||||||
|
project_id=str(project_id),
|
||||||
|
current_instance=json.dumps(
|
||||||
|
IssueSerializer(issue).data, cls=DjangoJSONEncoder
|
||||||
|
),
|
||||||
|
epoch=int(timezone.now().timestamp()),
|
||||||
|
notification=True,
|
||||||
|
origin=request.META.get("HTTP_ORIGIN"),
|
||||||
|
)
|
||||||
|
issue.archived_at = None
|
||||||
|
issue.save()
|
||||||
|
|
||||||
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
73
apiserver/plane/app/views/issue/attachment.py
Normal file
73
apiserver/plane/app/views/issue/attachment.py
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
# Python imports
|
||||||
|
import json
|
||||||
|
|
||||||
|
# Django imports
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.core.serializers.json import DjangoJSONEncoder
|
||||||
|
|
||||||
|
# Third Party imports
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.parsers import MultiPartParser, FormParser
|
||||||
|
|
||||||
|
# Module imports
|
||||||
|
from .. import BaseAPIView
|
||||||
|
from plane.app.serializers import IssueAttachmentSerializer
|
||||||
|
from plane.app.permissions import ProjectEntityPermission
|
||||||
|
from plane.db.models import IssueAttachment
|
||||||
|
from plane.bgtasks.issue_activites_task import issue_activity
|
||||||
|
|
||||||
|
|
||||||
|
class IssueAttachmentEndpoint(BaseAPIView):
|
||||||
|
serializer_class = IssueAttachmentSerializer
|
||||||
|
permission_classes = [
|
||||||
|
ProjectEntityPermission,
|
||||||
|
]
|
||||||
|
model = IssueAttachment
|
||||||
|
parser_classes = (MultiPartParser, FormParser)
|
||||||
|
|
||||||
|
def post(self, request, slug, project_id, issue_id):
|
||||||
|
serializer = IssueAttachmentSerializer(data=request.data)
|
||||||
|
if serializer.is_valid():
|
||||||
|
serializer.save(project_id=project_id, issue_id=issue_id)
|
||||||
|
issue_activity.delay(
|
||||||
|
type="attachment.activity.created",
|
||||||
|
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(
|
||||||
|
serializer.data,
|
||||||
|
cls=DjangoJSONEncoder,
|
||||||
|
),
|
||||||
|
epoch=int(timezone.now().timestamp()),
|
||||||
|
notification=True,
|
||||||
|
origin=request.META.get("HTTP_ORIGIN"),
|
||||||
|
)
|
||||||
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
def delete(self, request, slug, project_id, issue_id, pk):
|
||||||
|
issue_attachment = IssueAttachment.objects.get(pk=pk)
|
||||||
|
issue_attachment.asset.delete(save=False)
|
||||||
|
issue_attachment.delete()
|
||||||
|
issue_activity.delay(
|
||||||
|
type="attachment.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=None,
|
||||||
|
epoch=int(timezone.now().timestamp()),
|
||||||
|
notification=True,
|
||||||
|
origin=request.META.get("HTTP_ORIGIN"),
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
def get(self, request, slug, project_id, issue_id):
|
||||||
|
issue_attachments = IssueAttachment.objects.filter(
|
||||||
|
issue_id=issue_id, workspace__slug=slug, project_id=project_id
|
||||||
|
)
|
||||||
|
serializer = IssueAttachmentSerializer(issue_attachments, many=True)
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
686
apiserver/plane/app/views/issue/base.py
Normal file
686
apiserver/plane/app/views/issue/base.py
Normal file
@ -0,0 +1,686 @@
|
|||||||
|
# Python imports
|
||||||
|
import json
|
||||||
|
import random
|
||||||
|
from itertools import chain
|
||||||
|
|
||||||
|
# Django imports
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.db.models import (
|
||||||
|
Prefetch,
|
||||||
|
OuterRef,
|
||||||
|
Func,
|
||||||
|
F,
|
||||||
|
Q,
|
||||||
|
Case,
|
||||||
|
Value,
|
||||||
|
CharField,
|
||||||
|
When,
|
||||||
|
Exists,
|
||||||
|
Max,
|
||||||
|
)
|
||||||
|
from django.core.serializers.json import DjangoJSONEncoder
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
|
from django.views.decorators.gzip import gzip_page
|
||||||
|
from django.db import IntegrityError
|
||||||
|
from django.contrib.postgres.aggregates import ArrayAgg
|
||||||
|
from django.contrib.postgres.fields import ArrayField
|
||||||
|
from django.db.models import Value, UUIDField
|
||||||
|
from django.db.models.functions import Coalesce
|
||||||
|
|
||||||
|
# Third Party imports
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.parsers import MultiPartParser, FormParser
|
||||||
|
|
||||||
|
# Module imports
|
||||||
|
from .. import BaseViewSet, BaseAPIView, WebhookMixin
|
||||||
|
from plane.app.serializers import (
|
||||||
|
IssueActivitySerializer,
|
||||||
|
IssueCommentSerializer,
|
||||||
|
IssuePropertySerializer,
|
||||||
|
IssueSerializer,
|
||||||
|
IssueCreateSerializer,
|
||||||
|
LabelSerializer,
|
||||||
|
IssueFlatSerializer,
|
||||||
|
IssueLinkSerializer,
|
||||||
|
IssueLiteSerializer,
|
||||||
|
IssueAttachmentSerializer,
|
||||||
|
IssueSubscriberSerializer,
|
||||||
|
ProjectMemberLiteSerializer,
|
||||||
|
IssueReactionSerializer,
|
||||||
|
CommentReactionSerializer,
|
||||||
|
IssueRelationSerializer,
|
||||||
|
RelatedIssueSerializer,
|
||||||
|
IssueDetailSerializer,
|
||||||
|
)
|
||||||
|
from plane.app.permissions import (
|
||||||
|
ProjectEntityPermission,
|
||||||
|
WorkSpaceAdminPermission,
|
||||||
|
ProjectMemberPermission,
|
||||||
|
ProjectLitePermission,
|
||||||
|
)
|
||||||
|
from plane.db.models import (
|
||||||
|
Project,
|
||||||
|
Issue,
|
||||||
|
IssueActivity,
|
||||||
|
IssueComment,
|
||||||
|
IssueProperty,
|
||||||
|
Label,
|
||||||
|
IssueLink,
|
||||||
|
IssueAttachment,
|
||||||
|
IssueSubscriber,
|
||||||
|
ProjectMember,
|
||||||
|
IssueReaction,
|
||||||
|
CommentReaction,
|
||||||
|
IssueRelation,
|
||||||
|
)
|
||||||
|
from plane.bgtasks.issue_activites_task import issue_activity
|
||||||
|
from plane.utils.grouper import group_results
|
||||||
|
from plane.utils.issue_filters import issue_filters
|
||||||
|
from collections import defaultdict
|
||||||
|
from plane.utils.cache import invalidate_cache
|
||||||
|
|
||||||
|
class IssueListEndpoint(BaseAPIView):
|
||||||
|
|
||||||
|
permission_classes = [
|
||||||
|
ProjectEntityPermission,
|
||||||
|
]
|
||||||
|
|
||||||
|
def get(self, request, slug, project_id):
|
||||||
|
issue_ids = request.GET.get("issues", False)
|
||||||
|
|
||||||
|
if not issue_ids:
|
||||||
|
return Response(
|
||||||
|
{"error": "Issues are required"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
issue_ids = [
|
||||||
|
issue_id for issue_id in issue_ids.split(",") if issue_id != ""
|
||||||
|
]
|
||||||
|
|
||||||
|
queryset = (
|
||||||
|
Issue.issue_objects.filter(
|
||||||
|
workspace__slug=slug, project_id=project_id, pk__in=issue_ids
|
||||||
|
)
|
||||||
|
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||||
|
.select_related("workspace", "project", "state", "parent")
|
||||||
|
.prefetch_related("assignees", "labels", "issue_module__module")
|
||||||
|
.annotate(cycle_id=F("issue_cycle__cycle_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")
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
sub_issues_count=Issue.issue_objects.filter(
|
||||||
|
parent=OuterRef("id")
|
||||||
|
)
|
||||||
|
.order_by()
|
||||||
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
|
.values("count")
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
label_ids=Coalesce(
|
||||||
|
ArrayAgg(
|
||||||
|
"labels__id",
|
||||||
|
distinct=True,
|
||||||
|
filter=~Q(labels__id__isnull=True),
|
||||||
|
),
|
||||||
|
Value([], output_field=ArrayField(UUIDField())),
|
||||||
|
),
|
||||||
|
assignee_ids=Coalesce(
|
||||||
|
ArrayAgg(
|
||||||
|
"assignees__id",
|
||||||
|
distinct=True,
|
||||||
|
filter=~Q(assignees__id__isnull=True),
|
||||||
|
),
|
||||||
|
Value([], output_field=ArrayField(UUIDField())),
|
||||||
|
),
|
||||||
|
module_ids=Coalesce(
|
||||||
|
ArrayAgg(
|
||||||
|
"issue_module__module_id",
|
||||||
|
distinct=True,
|
||||||
|
filter=~Q(issue_module__module_id__isnull=True),
|
||||||
|
),
|
||||||
|
Value([], output_field=ArrayField(UUIDField())),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
).distinct()
|
||||||
|
|
||||||
|
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 = queryset.filter(**filters)
|
||||||
|
|
||||||
|
# Priority Ordering
|
||||||
|
if order_by_param == "priority" or order_by_param == "-priority":
|
||||||
|
priority_order = (
|
||||||
|
priority_order
|
||||||
|
if order_by_param == "priority"
|
||||||
|
else priority_order[::-1]
|
||||||
|
)
|
||||||
|
issue_queryset = issue_queryset.annotate(
|
||||||
|
priority_order=Case(
|
||||||
|
*[
|
||||||
|
When(priority=p, then=Value(i))
|
||||||
|
for i, p in enumerate(priority_order)
|
||||||
|
],
|
||||||
|
output_field=CharField(),
|
||||||
|
)
|
||||||
|
).order_by("priority_order")
|
||||||
|
|
||||||
|
# State Ordering
|
||||||
|
elif order_by_param in [
|
||||||
|
"state__name",
|
||||||
|
"state__group",
|
||||||
|
"-state__name",
|
||||||
|
"-state__group",
|
||||||
|
]:
|
||||||
|
state_order = (
|
||||||
|
state_order
|
||||||
|
if order_by_param in ["state__name", "state__group"]
|
||||||
|
else state_order[::-1]
|
||||||
|
)
|
||||||
|
issue_queryset = issue_queryset.annotate(
|
||||||
|
state_order=Case(
|
||||||
|
*[
|
||||||
|
When(state__group=state_group, then=Value(i))
|
||||||
|
for i, state_group in enumerate(state_order)
|
||||||
|
],
|
||||||
|
default=Value(len(state_order)),
|
||||||
|
output_field=CharField(),
|
||||||
|
)
|
||||||
|
).order_by("state_order")
|
||||||
|
# assignee and label ordering
|
||||||
|
elif order_by_param in [
|
||||||
|
"labels__name",
|
||||||
|
"-labels__name",
|
||||||
|
"assignees__first_name",
|
||||||
|
"-assignees__first_name",
|
||||||
|
]:
|
||||||
|
issue_queryset = issue_queryset.annotate(
|
||||||
|
max_values=Max(
|
||||||
|
order_by_param[1::]
|
||||||
|
if order_by_param.startswith("-")
|
||||||
|
else order_by_param
|
||||||
|
)
|
||||||
|
).order_by(
|
||||||
|
"-max_values"
|
||||||
|
if order_by_param.startswith("-")
|
||||||
|
else "max_values"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
issue_queryset = issue_queryset.order_by(order_by_param)
|
||||||
|
|
||||||
|
if self.fields or self.expand:
|
||||||
|
issues = IssueSerializer(
|
||||||
|
queryset, many=True, fields=self.fields, expand=self.expand
|
||||||
|
).data
|
||||||
|
else:
|
||||||
|
issues = issue_queryset.values(
|
||||||
|
"id",
|
||||||
|
"name",
|
||||||
|
"state_id",
|
||||||
|
"sort_order",
|
||||||
|
"completed_at",
|
||||||
|
"estimate_point",
|
||||||
|
"priority",
|
||||||
|
"start_date",
|
||||||
|
"target_date",
|
||||||
|
"sequence_id",
|
||||||
|
"project_id",
|
||||||
|
"parent_id",
|
||||||
|
"cycle_id",
|
||||||
|
"module_ids",
|
||||||
|
"label_ids",
|
||||||
|
"assignee_ids",
|
||||||
|
"sub_issues_count",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
"created_by",
|
||||||
|
"updated_by",
|
||||||
|
"attachment_count",
|
||||||
|
"link_count",
|
||||||
|
"is_draft",
|
||||||
|
"archived_at",
|
||||||
|
)
|
||||||
|
return Response(issues, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
|
class IssueViewSet(WebhookMixin, BaseViewSet):
|
||||||
|
def get_serializer_class(self):
|
||||||
|
return (
|
||||||
|
IssueCreateSerializer
|
||||||
|
if self.action in ["create", "update", "partial_update"]
|
||||||
|
else IssueSerializer
|
||||||
|
)
|
||||||
|
|
||||||
|
model = Issue
|
||||||
|
webhook_event = "issue"
|
||||||
|
permission_classes = [
|
||||||
|
ProjectEntityPermission,
|
||||||
|
]
|
||||||
|
|
||||||
|
search_fields = [
|
||||||
|
"name",
|
||||||
|
]
|
||||||
|
|
||||||
|
filterset_fields = [
|
||||||
|
"state__name",
|
||||||
|
"assignees__id",
|
||||||
|
"workspace__id",
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return (
|
||||||
|
Issue.issue_objects.filter(
|
||||||
|
project_id=self.kwargs.get("project_id")
|
||||||
|
)
|
||||||
|
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||||
|
.select_related("workspace", "project", "state", "parent")
|
||||||
|
.prefetch_related("assignees", "labels", "issue_module__module")
|
||||||
|
.annotate(cycle_id=F("issue_cycle__cycle_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")
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
sub_issues_count=Issue.issue_objects.filter(
|
||||||
|
parent=OuterRef("id")
|
||||||
|
)
|
||||||
|
.order_by()
|
||||||
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
|
.values("count")
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
label_ids=Coalesce(
|
||||||
|
ArrayAgg(
|
||||||
|
"labels__id",
|
||||||
|
distinct=True,
|
||||||
|
filter=~Q(labels__id__isnull=True),
|
||||||
|
),
|
||||||
|
Value([], output_field=ArrayField(UUIDField())),
|
||||||
|
),
|
||||||
|
assignee_ids=Coalesce(
|
||||||
|
ArrayAgg(
|
||||||
|
"assignees__id",
|
||||||
|
distinct=True,
|
||||||
|
filter=~Q(assignees__id__isnull=True),
|
||||||
|
),
|
||||||
|
Value([], output_field=ArrayField(UUIDField())),
|
||||||
|
),
|
||||||
|
module_ids=Coalesce(
|
||||||
|
ArrayAgg(
|
||||||
|
"issue_module__module_id",
|
||||||
|
distinct=True,
|
||||||
|
filter=~Q(issue_module__module_id__isnull=True),
|
||||||
|
),
|
||||||
|
Value([], output_field=ArrayField(UUIDField())),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
).distinct()
|
||||||
|
|
||||||
|
@method_decorator(gzip_page)
|
||||||
|
def list(self, request, slug, project_id):
|
||||||
|
filters = issue_filters(request.query_params, "GET")
|
||||||
|
order_by_param = request.GET.get("order_by", "-created_at")
|
||||||
|
|
||||||
|
issue_queryset = self.get_queryset().filter(**filters)
|
||||||
|
# Custom ordering for priority and state
|
||||||
|
priority_order = ["urgent", "high", "medium", "low", "none"]
|
||||||
|
state_order = [
|
||||||
|
"backlog",
|
||||||
|
"unstarted",
|
||||||
|
"started",
|
||||||
|
"completed",
|
||||||
|
"cancelled",
|
||||||
|
]
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
# Only use serializer when expand or fields else return by values
|
||||||
|
if self.expand or self.fields:
|
||||||
|
issues = IssueSerializer(
|
||||||
|
issue_queryset,
|
||||||
|
many=True,
|
||||||
|
fields=self.fields,
|
||||||
|
expand=self.expand,
|
||||||
|
).data
|
||||||
|
else:
|
||||||
|
issues = issue_queryset.values(
|
||||||
|
"id",
|
||||||
|
"name",
|
||||||
|
"state_id",
|
||||||
|
"sort_order",
|
||||||
|
"completed_at",
|
||||||
|
"estimate_point",
|
||||||
|
"priority",
|
||||||
|
"start_date",
|
||||||
|
"target_date",
|
||||||
|
"sequence_id",
|
||||||
|
"project_id",
|
||||||
|
"parent_id",
|
||||||
|
"cycle_id",
|
||||||
|
"module_ids",
|
||||||
|
"label_ids",
|
||||||
|
"assignee_ids",
|
||||||
|
"sub_issues_count",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
"created_by",
|
||||||
|
"updated_by",
|
||||||
|
"attachment_count",
|
||||||
|
"link_count",
|
||||||
|
"is_draft",
|
||||||
|
"archived_at",
|
||||||
|
)
|
||||||
|
return Response(issues, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
def create(self, request, slug, project_id):
|
||||||
|
project = Project.objects.get(pk=project_id)
|
||||||
|
|
||||||
|
serializer = IssueCreateSerializer(
|
||||||
|
data=request.data,
|
||||||
|
context={
|
||||||
|
"project_id": project_id,
|
||||||
|
"workspace_id": project.workspace_id,
|
||||||
|
"default_assignee_id": project.default_assignee_id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if serializer.is_valid():
|
||||||
|
serializer.save()
|
||||||
|
|
||||||
|
# Track the issue
|
||||||
|
issue_activity.delay(
|
||||||
|
type="issue.activity.created",
|
||||||
|
requested_data=json.dumps(
|
||||||
|
self.request.data, cls=DjangoJSONEncoder
|
||||||
|
),
|
||||||
|
actor_id=str(request.user.id),
|
||||||
|
issue_id=str(serializer.data.get("id", None)),
|
||||||
|
project_id=str(project_id),
|
||||||
|
current_instance=None,
|
||||||
|
epoch=int(timezone.now().timestamp()),
|
||||||
|
notification=True,
|
||||||
|
origin=request.META.get("HTTP_ORIGIN"),
|
||||||
|
)
|
||||||
|
issue = (
|
||||||
|
self.get_queryset()
|
||||||
|
.filter(pk=serializer.data["id"])
|
||||||
|
.values(
|
||||||
|
"id",
|
||||||
|
"name",
|
||||||
|
"state_id",
|
||||||
|
"sort_order",
|
||||||
|
"completed_at",
|
||||||
|
"estimate_point",
|
||||||
|
"priority",
|
||||||
|
"start_date",
|
||||||
|
"target_date",
|
||||||
|
"sequence_id",
|
||||||
|
"project_id",
|
||||||
|
"parent_id",
|
||||||
|
"cycle_id",
|
||||||
|
"module_ids",
|
||||||
|
"label_ids",
|
||||||
|
"assignee_ids",
|
||||||
|
"sub_issues_count",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
"created_by",
|
||||||
|
"updated_by",
|
||||||
|
"attachment_count",
|
||||||
|
"link_count",
|
||||||
|
"is_draft",
|
||||||
|
"archived_at",
|
||||||
|
)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
return Response(issue, status=status.HTTP_201_CREATED)
|
||||||
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
def retrieve(self, request, slug, project_id, pk=None):
|
||||||
|
issue = (
|
||||||
|
self.get_queryset()
|
||||||
|
.filter(pk=pk)
|
||||||
|
.prefetch_related(
|
||||||
|
Prefetch(
|
||||||
|
"issue_reactions",
|
||||||
|
queryset=IssueReaction.objects.select_related(
|
||||||
|
"issue", "actor"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.prefetch_related(
|
||||||
|
Prefetch(
|
||||||
|
"issue_attachment",
|
||||||
|
queryset=IssueAttachment.objects.select_related("issue"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.prefetch_related(
|
||||||
|
Prefetch(
|
||||||
|
"issue_link",
|
||||||
|
queryset=IssueLink.objects.select_related("created_by"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
is_subscribed=Exists(
|
||||||
|
IssueSubscriber.objects.filter(
|
||||||
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
issue_id=OuterRef("pk"),
|
||||||
|
subscriber=request.user,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
).first()
|
||||||
|
if not issue:
|
||||||
|
return Response(
|
||||||
|
{"error": "The required object does not exist."},
|
||||||
|
status=status.HTTP_404_NOT_FOUND,
|
||||||
|
)
|
||||||
|
|
||||||
|
serializer = IssueDetailSerializer(issue, expand=self.expand)
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
def partial_update(self, request, slug, project_id, pk=None):
|
||||||
|
issue = self.get_queryset().filter(pk=pk).first()
|
||||||
|
|
||||||
|
if not issue:
|
||||||
|
return Response(
|
||||||
|
{"error": "Issue not found"},
|
||||||
|
status=status.HTTP_404_NOT_FOUND,
|
||||||
|
)
|
||||||
|
|
||||||
|
current_instance = json.dumps(
|
||||||
|
IssueSerializer(issue).data, cls=DjangoJSONEncoder
|
||||||
|
)
|
||||||
|
|
||||||
|
requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder)
|
||||||
|
serializer = IssueCreateSerializer(
|
||||||
|
issue, data=request.data, partial=True
|
||||||
|
)
|
||||||
|
if serializer.is_valid():
|
||||||
|
serializer.save()
|
||||||
|
issue_activity.delay(
|
||||||
|
type="issue.activity.updated",
|
||||||
|
requested_data=requested_data,
|
||||||
|
actor_id=str(request.user.id),
|
||||||
|
issue_id=str(pk),
|
||||||
|
project_id=str(project_id),
|
||||||
|
current_instance=current_instance,
|
||||||
|
epoch=int(timezone.now().timestamp()),
|
||||||
|
notification=True,
|
||||||
|
origin=request.META.get("HTTP_ORIGIN"),
|
||||||
|
)
|
||||||
|
issue = self.get_queryset().filter(pk=pk).first()
|
||||||
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
def destroy(self, request, slug, project_id, pk=None):
|
||||||
|
issue = Issue.objects.get(
|
||||||
|
workspace__slug=slug, project_id=project_id, pk=pk
|
||||||
|
)
|
||||||
|
issue.delete()
|
||||||
|
issue_activity.delay(
|
||||||
|
type="issue.activity.deleted",
|
||||||
|
requested_data=json.dumps({"issue_id": str(pk)}),
|
||||||
|
actor_id=str(request.user.id),
|
||||||
|
issue_id=str(pk),
|
||||||
|
project_id=str(project_id),
|
||||||
|
current_instance={},
|
||||||
|
epoch=int(timezone.now().timestamp()),
|
||||||
|
notification=True,
|
||||||
|
origin=request.META.get("HTTP_ORIGIN"),
|
||||||
|
)
|
||||||
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
|
||||||
|
class IssueUserDisplayPropertyEndpoint(BaseAPIView):
|
||||||
|
permission_classes = [
|
||||||
|
ProjectLitePermission,
|
||||||
|
]
|
||||||
|
|
||||||
|
def patch(self, request, slug, project_id):
|
||||||
|
issue_property = IssueProperty.objects.get(
|
||||||
|
user=request.user,
|
||||||
|
project_id=project_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
issue_property.filters = request.data.get(
|
||||||
|
"filters", issue_property.filters
|
||||||
|
)
|
||||||
|
issue_property.display_filters = request.data.get(
|
||||||
|
"display_filters", issue_property.display_filters
|
||||||
|
)
|
||||||
|
issue_property.display_properties = request.data.get(
|
||||||
|
"display_properties", issue_property.display_properties
|
||||||
|
)
|
||||||
|
issue_property.save()
|
||||||
|
serializer = IssuePropertySerializer(issue_property)
|
||||||
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
|
def get(self, request, slug, project_id):
|
||||||
|
issue_property, _ = IssueProperty.objects.get_or_create(
|
||||||
|
user=request.user, project_id=project_id
|
||||||
|
)
|
||||||
|
serializer = IssuePropertySerializer(issue_property)
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
|
class BulkDeleteIssuesEndpoint(BaseAPIView):
|
||||||
|
permission_classes = [
|
||||||
|
ProjectEntityPermission,
|
||||||
|
]
|
||||||
|
|
||||||
|
def delete(self, request, slug, project_id):
|
||||||
|
issue_ids = request.data.get("issue_ids", [])
|
||||||
|
|
||||||
|
if not len(issue_ids):
|
||||||
|
return Response(
|
||||||
|
{"error": "Issue IDs are required"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
issues = Issue.issue_objects.filter(
|
||||||
|
workspace__slug=slug, project_id=project_id, pk__in=issue_ids
|
||||||
|
)
|
||||||
|
|
||||||
|
total_issues = len(issues)
|
||||||
|
|
||||||
|
issues.delete()
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
{"message": f"{total_issues} issues were deleted"},
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
219
apiserver/plane/app/views/issue/comment.py
Normal file
219
apiserver/plane/app/views/issue/comment.py
Normal file
@ -0,0 +1,219 @@
|
|||||||
|
# Python imports
|
||||||
|
import json
|
||||||
|
|
||||||
|
# Django imports
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.db.models import Exists
|
||||||
|
from django.core.serializers.json import DjangoJSONEncoder
|
||||||
|
|
||||||
|
# Third Party imports
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework import status
|
||||||
|
|
||||||
|
# Module imports
|
||||||
|
from .. import BaseViewSet, WebhookMixin
|
||||||
|
from plane.app.serializers import (
|
||||||
|
IssueCommentSerializer,
|
||||||
|
CommentReactionSerializer,
|
||||||
|
)
|
||||||
|
from plane.app.permissions import ProjectLitePermission
|
||||||
|
from plane.db.models import (
|
||||||
|
IssueComment,
|
||||||
|
ProjectMember,
|
||||||
|
CommentReaction,
|
||||||
|
)
|
||||||
|
from plane.bgtasks.issue_activites_task import issue_activity
|
||||||
|
|
||||||
|
|
||||||
|
class IssueCommentViewSet(WebhookMixin, BaseViewSet):
|
||||||
|
serializer_class = IssueCommentSerializer
|
||||||
|
model = IssueComment
|
||||||
|
webhook_event = "issue_comment"
|
||||||
|
permission_classes = [
|
||||||
|
ProjectLitePermission,
|
||||||
|
]
|
||||||
|
|
||||||
|
filterset_fields = [
|
||||||
|
"issue__id",
|
||||||
|
"workspace__id",
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return self.filter_queryset(
|
||||||
|
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"))
|
||||||
|
.filter(
|
||||||
|
project__project_projectmember__member=self.request.user,
|
||||||
|
project__project_projectmember__is_active=True,
|
||||||
|
)
|
||||||
|
.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()
|
||||||
|
)
|
||||||
|
|
||||||
|
def create(self, request, slug, project_id, issue_id):
|
||||||
|
serializer = IssueCommentSerializer(data=request.data)
|
||||||
|
if serializer.is_valid():
|
||||||
|
serializer.save(
|
||||||
|
project_id=project_id,
|
||||||
|
issue_id=issue_id,
|
||||||
|
actor=request.user,
|
||||||
|
)
|
||||||
|
issue_activity.delay(
|
||||||
|
type="comment.activity.created",
|
||||||
|
requested_data=json.dumps(
|
||||||
|
serializer.data, cls=DjangoJSONEncoder
|
||||||
|
),
|
||||||
|
actor_id=str(self.request.user.id),
|
||||||
|
issue_id=str(self.kwargs.get("issue_id")),
|
||||||
|
project_id=str(self.kwargs.get("project_id")),
|
||||||
|
current_instance=None,
|
||||||
|
epoch=int(timezone.now().timestamp()),
|
||||||
|
notification=True,
|
||||||
|
origin=request.META.get("HTTP_ORIGIN"),
|
||||||
|
)
|
||||||
|
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):
|
||||||
|
issue_comment = IssueComment.objects.get(
|
||||||
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
issue_id=issue_id,
|
||||||
|
pk=pk,
|
||||||
|
)
|
||||||
|
requested_data = json.dumps(self.request.data, cls=DjangoJSONEncoder)
|
||||||
|
current_instance = json.dumps(
|
||||||
|
IssueCommentSerializer(issue_comment).data,
|
||||||
|
cls=DjangoJSONEncoder,
|
||||||
|
)
|
||||||
|
serializer = IssueCommentSerializer(
|
||||||
|
issue_comment, data=request.data, partial=True
|
||||||
|
)
|
||||||
|
if serializer.is_valid():
|
||||||
|
serializer.save()
|
||||||
|
issue_activity.delay(
|
||||||
|
type="comment.activity.updated",
|
||||||
|
requested_data=requested_data,
|
||||||
|
actor_id=str(request.user.id),
|
||||||
|
issue_id=str(issue_id),
|
||||||
|
project_id=str(project_id),
|
||||||
|
current_instance=current_instance,
|
||||||
|
epoch=int(timezone.now().timestamp()),
|
||||||
|
notification=True,
|
||||||
|
origin=request.META.get("HTTP_ORIGIN"),
|
||||||
|
)
|
||||||
|
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):
|
||||||
|
issue_comment = IssueComment.objects.get(
|
||||||
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
issue_id=issue_id,
|
||||||
|
pk=pk,
|
||||||
|
)
|
||||||
|
current_instance = json.dumps(
|
||||||
|
IssueCommentSerializer(issue_comment).data,
|
||||||
|
cls=DjangoJSONEncoder,
|
||||||
|
)
|
||||||
|
issue_comment.delete()
|
||||||
|
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=current_instance,
|
||||||
|
epoch=int(timezone.now().timestamp()),
|
||||||
|
notification=True,
|
||||||
|
origin=request.META.get("HTTP_ORIGIN"),
|
||||||
|
)
|
||||||
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
|
||||||
|
class CommentReactionViewSet(BaseViewSet):
|
||||||
|
serializer_class = CommentReactionSerializer
|
||||||
|
model = CommentReaction
|
||||||
|
permission_classes = [
|
||||||
|
ProjectLitePermission,
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
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"))
|
||||||
|
.filter(
|
||||||
|
project__project_projectmember__member=self.request.user,
|
||||||
|
project__project_projectmember__is_active=True,
|
||||||
|
)
|
||||||
|
.order_by("-created_at")
|
||||||
|
.distinct()
|
||||||
|
)
|
||||||
|
|
||||||
|
def create(self, request, slug, project_id, comment_id):
|
||||||
|
serializer = CommentReactionSerializer(data=request.data)
|
||||||
|
if serializer.is_valid():
|
||||||
|
serializer.save(
|
||||||
|
project_id=project_id,
|
||||||
|
actor_id=request.user.id,
|
||||||
|
comment_id=comment_id,
|
||||||
|
)
|
||||||
|
issue_activity.delay(
|
||||||
|
type="comment_reaction.activity.created",
|
||||||
|
requested_data=json.dumps(request.data, cls=DjangoJSONEncoder),
|
||||||
|
actor_id=str(request.user.id),
|
||||||
|
issue_id=None,
|
||||||
|
project_id=str(project_id),
|
||||||
|
current_instance=None,
|
||||||
|
epoch=int(timezone.now().timestamp()),
|
||||||
|
notification=True,
|
||||||
|
origin=request.META.get("HTTP_ORIGIN"),
|
||||||
|
)
|
||||||
|
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):
|
||||||
|
comment_reaction = CommentReaction.objects.get(
|
||||||
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
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()),
|
||||||
|
notification=True,
|
||||||
|
origin=request.META.get("HTTP_ORIGIN"),
|
||||||
|
)
|
||||||
|
comment_reaction.delete()
|
||||||
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
367
apiserver/plane/app/views/issue/draft.py
Normal file
367
apiserver/plane/app/views/issue/draft.py
Normal file
@ -0,0 +1,367 @@
|
|||||||
|
# Python imports
|
||||||
|
import json
|
||||||
|
|
||||||
|
# Django imports
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.db.models import (
|
||||||
|
Prefetch,
|
||||||
|
OuterRef,
|
||||||
|
Func,
|
||||||
|
F,
|
||||||
|
Q,
|
||||||
|
Case,
|
||||||
|
Value,
|
||||||
|
CharField,
|
||||||
|
When,
|
||||||
|
Exists,
|
||||||
|
Max,
|
||||||
|
UUIDField,
|
||||||
|
)
|
||||||
|
from django.core.serializers.json import DjangoJSONEncoder
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
|
from django.views.decorators.gzip import gzip_page
|
||||||
|
from django.contrib.postgres.aggregates import ArrayAgg
|
||||||
|
from django.contrib.postgres.fields import ArrayField
|
||||||
|
from django.db.models.functions import Coalesce
|
||||||
|
|
||||||
|
# Third Party imports
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework import status
|
||||||
|
|
||||||
|
# Module imports
|
||||||
|
from .. import BaseViewSet
|
||||||
|
from plane.app.serializers import (
|
||||||
|
IssueSerializer,
|
||||||
|
IssueCreateSerializer,
|
||||||
|
IssueFlatSerializer,
|
||||||
|
IssueDetailSerializer,
|
||||||
|
)
|
||||||
|
from plane.app.permissions import ProjectEntityPermission
|
||||||
|
from plane.db.models import (
|
||||||
|
Project,
|
||||||
|
Issue,
|
||||||
|
IssueLink,
|
||||||
|
IssueAttachment,
|
||||||
|
IssueSubscriber,
|
||||||
|
IssueReaction,
|
||||||
|
)
|
||||||
|
from plane.bgtasks.issue_activites_task import issue_activity
|
||||||
|
from plane.utils.issue_filters import issue_filters
|
||||||
|
|
||||||
|
|
||||||
|
class IssueDraftViewSet(BaseViewSet):
|
||||||
|
permission_classes = [
|
||||||
|
ProjectEntityPermission,
|
||||||
|
]
|
||||||
|
serializer_class = IssueFlatSerializer
|
||||||
|
model = Issue
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return (
|
||||||
|
Issue.objects.filter(project_id=self.kwargs.get("project_id"))
|
||||||
|
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||||
|
.filter(is_draft=True)
|
||||||
|
.select_related("workspace", "project", "state", "parent")
|
||||||
|
.prefetch_related("assignees", "labels", "issue_module__module")
|
||||||
|
.annotate(cycle_id=F("issue_cycle__cycle_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")
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
sub_issues_count=Issue.issue_objects.filter(
|
||||||
|
parent=OuterRef("id")
|
||||||
|
)
|
||||||
|
.order_by()
|
||||||
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
|
.values("count")
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
label_ids=Coalesce(
|
||||||
|
ArrayAgg(
|
||||||
|
"labels__id",
|
||||||
|
distinct=True,
|
||||||
|
filter=~Q(labels__id__isnull=True),
|
||||||
|
),
|
||||||
|
Value([], output_field=ArrayField(UUIDField())),
|
||||||
|
),
|
||||||
|
assignee_ids=Coalesce(
|
||||||
|
ArrayAgg(
|
||||||
|
"assignees__id",
|
||||||
|
distinct=True,
|
||||||
|
filter=~Q(assignees__id__isnull=True),
|
||||||
|
),
|
||||||
|
Value([], output_field=ArrayField(UUIDField())),
|
||||||
|
),
|
||||||
|
module_ids=Coalesce(
|
||||||
|
ArrayAgg(
|
||||||
|
"issue_module__module_id",
|
||||||
|
distinct=True,
|
||||||
|
filter=~Q(issue_module__module_id__isnull=True),
|
||||||
|
),
|
||||||
|
Value([], output_field=ArrayField(UUIDField())),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
).distinct()
|
||||||
|
|
||||||
|
@method_decorator(gzip_page)
|
||||||
|
def list(self, request, slug, project_id):
|
||||||
|
filters = issue_filters(request.query_params, "GET")
|
||||||
|
fields = [
|
||||||
|
field
|
||||||
|
for field in request.GET.get("fields", "").split(",")
|
||||||
|
if field
|
||||||
|
]
|
||||||
|
|
||||||
|
# Custom ordering for priority and state
|
||||||
|
priority_order = ["urgent", "high", "medium", "low", "none"]
|
||||||
|
state_order = [
|
||||||
|
"backlog",
|
||||||
|
"unstarted",
|
||||||
|
"started",
|
||||||
|
"completed",
|
||||||
|
"cancelled",
|
||||||
|
]
|
||||||
|
|
||||||
|
order_by_param = request.GET.get("order_by", "-created_at")
|
||||||
|
|
||||||
|
issue_queryset = self.get_queryset().filter(**filters)
|
||||||
|
|
||||||
|
# Priority Ordering
|
||||||
|
if order_by_param == "priority" or order_by_param == "-priority":
|
||||||
|
priority_order = (
|
||||||
|
priority_order
|
||||||
|
if order_by_param == "priority"
|
||||||
|
else priority_order[::-1]
|
||||||
|
)
|
||||||
|
issue_queryset = issue_queryset.annotate(
|
||||||
|
priority_order=Case(
|
||||||
|
*[
|
||||||
|
When(priority=p, then=Value(i))
|
||||||
|
for i, p in enumerate(priority_order)
|
||||||
|
],
|
||||||
|
output_field=CharField(),
|
||||||
|
)
|
||||||
|
).order_by("priority_order")
|
||||||
|
|
||||||
|
# State Ordering
|
||||||
|
elif order_by_param in [
|
||||||
|
"state__name",
|
||||||
|
"state__group",
|
||||||
|
"-state__name",
|
||||||
|
"-state__group",
|
||||||
|
]:
|
||||||
|
state_order = (
|
||||||
|
state_order
|
||||||
|
if order_by_param in ["state__name", "state__group"]
|
||||||
|
else state_order[::-1]
|
||||||
|
)
|
||||||
|
issue_queryset = issue_queryset.annotate(
|
||||||
|
state_order=Case(
|
||||||
|
*[
|
||||||
|
When(state__group=state_group, then=Value(i))
|
||||||
|
for i, state_group in enumerate(state_order)
|
||||||
|
],
|
||||||
|
default=Value(len(state_order)),
|
||||||
|
output_field=CharField(),
|
||||||
|
)
|
||||||
|
).order_by("state_order")
|
||||||
|
# assignee and label ordering
|
||||||
|
elif order_by_param in [
|
||||||
|
"labels__name",
|
||||||
|
"-labels__name",
|
||||||
|
"assignees__first_name",
|
||||||
|
"-assignees__first_name",
|
||||||
|
]:
|
||||||
|
issue_queryset = issue_queryset.annotate(
|
||||||
|
max_values=Max(
|
||||||
|
order_by_param[1::]
|
||||||
|
if order_by_param.startswith("-")
|
||||||
|
else order_by_param
|
||||||
|
)
|
||||||
|
).order_by(
|
||||||
|
"-max_values"
|
||||||
|
if order_by_param.startswith("-")
|
||||||
|
else "max_values"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
issue_queryset = issue_queryset.order_by(order_by_param)
|
||||||
|
|
||||||
|
# Only use serializer when expand else return by values
|
||||||
|
if self.expand or self.fields:
|
||||||
|
issues = IssueSerializer(
|
||||||
|
issue_queryset,
|
||||||
|
many=True,
|
||||||
|
fields=self.fields,
|
||||||
|
expand=self.expand,
|
||||||
|
).data
|
||||||
|
else:
|
||||||
|
issues = issue_queryset.values(
|
||||||
|
"id",
|
||||||
|
"name",
|
||||||
|
"state_id",
|
||||||
|
"sort_order",
|
||||||
|
"completed_at",
|
||||||
|
"estimate_point",
|
||||||
|
"priority",
|
||||||
|
"start_date",
|
||||||
|
"target_date",
|
||||||
|
"sequence_id",
|
||||||
|
"project_id",
|
||||||
|
"parent_id",
|
||||||
|
"cycle_id",
|
||||||
|
"module_ids",
|
||||||
|
"label_ids",
|
||||||
|
"assignee_ids",
|
||||||
|
"sub_issues_count",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
"created_by",
|
||||||
|
"updated_by",
|
||||||
|
"attachment_count",
|
||||||
|
"link_count",
|
||||||
|
"is_draft",
|
||||||
|
"archived_at",
|
||||||
|
)
|
||||||
|
return Response(issues, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
def create(self, request, slug, project_id):
|
||||||
|
project = Project.objects.get(pk=project_id)
|
||||||
|
|
||||||
|
serializer = IssueCreateSerializer(
|
||||||
|
data=request.data,
|
||||||
|
context={
|
||||||
|
"project_id": project_id,
|
||||||
|
"workspace_id": project.workspace_id,
|
||||||
|
"default_assignee_id": project.default_assignee_id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if serializer.is_valid():
|
||||||
|
serializer.save(is_draft=True)
|
||||||
|
|
||||||
|
# Track the issue
|
||||||
|
issue_activity.delay(
|
||||||
|
type="issue_draft.activity.created",
|
||||||
|
requested_data=json.dumps(
|
||||||
|
self.request.data, cls=DjangoJSONEncoder
|
||||||
|
),
|
||||||
|
actor_id=str(request.user.id),
|
||||||
|
issue_id=str(serializer.data.get("id", None)),
|
||||||
|
project_id=str(project_id),
|
||||||
|
current_instance=None,
|
||||||
|
epoch=int(timezone.now().timestamp()),
|
||||||
|
notification=True,
|
||||||
|
origin=request.META.get("HTTP_ORIGIN"),
|
||||||
|
)
|
||||||
|
issue = (
|
||||||
|
self.get_queryset().filter(pk=serializer.data["id"]).first()
|
||||||
|
)
|
||||||
|
return Response(
|
||||||
|
IssueSerializer(issue).data, status=status.HTTP_201_CREATED
|
||||||
|
)
|
||||||
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
def partial_update(self, request, slug, project_id, pk):
|
||||||
|
issue = self.get_queryset().filter(pk=pk).first()
|
||||||
|
|
||||||
|
if not issue:
|
||||||
|
return Response(
|
||||||
|
{"error": "Issue does not exist"},
|
||||||
|
status=status.HTTP_404_NOT_FOUND,
|
||||||
|
)
|
||||||
|
|
||||||
|
serializer = IssueCreateSerializer(
|
||||||
|
issue, data=request.data, partial=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if serializer.is_valid():
|
||||||
|
serializer.save()
|
||||||
|
issue_activity.delay(
|
||||||
|
type="issue_draft.activity.updated",
|
||||||
|
requested_data=json.dumps(request.data, cls=DjangoJSONEncoder),
|
||||||
|
actor_id=str(self.request.user.id),
|
||||||
|
issue_id=str(self.kwargs.get("pk", None)),
|
||||||
|
project_id=str(self.kwargs.get("project_id", None)),
|
||||||
|
current_instance=json.dumps(
|
||||||
|
IssueSerializer(issue).data,
|
||||||
|
cls=DjangoJSONEncoder,
|
||||||
|
),
|
||||||
|
epoch=int(timezone.now().timestamp()),
|
||||||
|
notification=True,
|
||||||
|
origin=request.META.get("HTTP_ORIGIN"),
|
||||||
|
)
|
||||||
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
def retrieve(self, request, slug, project_id, pk=None):
|
||||||
|
issue = (
|
||||||
|
self.get_queryset()
|
||||||
|
.filter(pk=pk)
|
||||||
|
.prefetch_related(
|
||||||
|
Prefetch(
|
||||||
|
"issue_reactions",
|
||||||
|
queryset=IssueReaction.objects.select_related(
|
||||||
|
"issue", "actor"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.prefetch_related(
|
||||||
|
Prefetch(
|
||||||
|
"issue_attachment",
|
||||||
|
queryset=IssueAttachment.objects.select_related("issue"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.prefetch_related(
|
||||||
|
Prefetch(
|
||||||
|
"issue_link",
|
||||||
|
queryset=IssueLink.objects.select_related("created_by"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
is_subscribed=Exists(
|
||||||
|
IssueSubscriber.objects.filter(
|
||||||
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
issue_id=OuterRef("pk"),
|
||||||
|
subscriber=request.user,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if not issue:
|
||||||
|
return Response(
|
||||||
|
{"error": "The required object does not exist."},
|
||||||
|
status=status.HTTP_404_NOT_FOUND,
|
||||||
|
)
|
||||||
|
serializer = IssueDetailSerializer(issue, expand=self.expand)
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
def destroy(self, request, slug, project_id, pk=None):
|
||||||
|
issue = Issue.objects.get(
|
||||||
|
workspace__slug=slug, project_id=project_id, pk=pk
|
||||||
|
)
|
||||||
|
issue.delete()
|
||||||
|
issue_activity.delay(
|
||||||
|
type="issue_draft.activity.deleted",
|
||||||
|
requested_data=json.dumps({"issue_id": str(pk)}),
|
||||||
|
actor_id=str(request.user.id),
|
||||||
|
issue_id=str(pk),
|
||||||
|
project_id=str(project_id),
|
||||||
|
current_instance={},
|
||||||
|
epoch=int(timezone.now().timestamp()),
|
||||||
|
notification=True,
|
||||||
|
origin=request.META.get("HTTP_ORIGIN"),
|
||||||
|
)
|
||||||
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
105
apiserver/plane/app/views/issue/label.py
Normal file
105
apiserver/plane/app/views/issue/label.py
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
# Python imports
|
||||||
|
import random
|
||||||
|
|
||||||
|
# Django imports
|
||||||
|
from django.db import IntegrityError
|
||||||
|
|
||||||
|
# Third Party imports
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework import status
|
||||||
|
|
||||||
|
# Module imports
|
||||||
|
from .. import BaseViewSet, BaseAPIView
|
||||||
|
from plane.app.serializers import LabelSerializer
|
||||||
|
from plane.app.permissions import (
|
||||||
|
ProjectMemberPermission,
|
||||||
|
)
|
||||||
|
from plane.db.models import (
|
||||||
|
Project,
|
||||||
|
Label,
|
||||||
|
)
|
||||||
|
from plane.utils.cache import invalidate_cache
|
||||||
|
|
||||||
|
|
||||||
|
class LabelViewSet(BaseViewSet):
|
||||||
|
serializer_class = LabelSerializer
|
||||||
|
model = Label
|
||||||
|
permission_classes = [
|
||||||
|
ProjectMemberPermission,
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return self.filter_queryset(
|
||||||
|
super()
|
||||||
|
.get_queryset()
|
||||||
|
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||||
|
.filter(project_id=self.kwargs.get("project_id"))
|
||||||
|
.filter(project__project_projectmember__member=self.request.user)
|
||||||
|
.select_related("project")
|
||||||
|
.select_related("workspace")
|
||||||
|
.select_related("parent")
|
||||||
|
.distinct()
|
||||||
|
.order_by("sort_order")
|
||||||
|
)
|
||||||
|
|
||||||
|
@invalidate_cache(
|
||||||
|
path="/api/workspaces/:slug/labels/", url_params=True, user=False
|
||||||
|
)
|
||||||
|
def create(self, request, slug, project_id):
|
||||||
|
try:
|
||||||
|
serializer = LabelSerializer(data=request.data)
|
||||||
|
if serializer.is_valid():
|
||||||
|
serializer.save(project_id=project_id)
|
||||||
|
return Response(
|
||||||
|
serializer.data, status=status.HTTP_201_CREATED
|
||||||
|
)
|
||||||
|
return Response(
|
||||||
|
serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
|
except IntegrityError:
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"error": "Label with the same name already exists in the project"
|
||||||
|
},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
@invalidate_cache(
|
||||||
|
path="/api/workspaces/:slug/labels/", url_params=True, user=False
|
||||||
|
)
|
||||||
|
def partial_update(self, request, *args, **kwargs):
|
||||||
|
return super().partial_update(request, *args, **kwargs)
|
||||||
|
|
||||||
|
@invalidate_cache(
|
||||||
|
path="/api/workspaces/:slug/labels/", url_params=True, user=False
|
||||||
|
)
|
||||||
|
def destroy(self, request, *args, **kwargs):
|
||||||
|
return super().destroy(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class BulkCreateIssueLabelsEndpoint(BaseAPIView):
|
||||||
|
def post(self, request, slug, project_id):
|
||||||
|
label_data = request.data.get("label_data", [])
|
||||||
|
project = Project.objects.get(pk=project_id)
|
||||||
|
|
||||||
|
labels = Label.objects.bulk_create(
|
||||||
|
[
|
||||||
|
Label(
|
||||||
|
name=label.get("name", "Migrated"),
|
||||||
|
description=label.get("description", "Migrated Issue"),
|
||||||
|
color="#" + "%06x" % random.randint(0, 0xFFFFFF),
|
||||||
|
project_id=project_id,
|
||||||
|
workspace_id=project.workspace_id,
|
||||||
|
created_by=request.user,
|
||||||
|
updated_by=request.user,
|
||||||
|
)
|
||||||
|
for label in label_data
|
||||||
|
],
|
||||||
|
batch_size=50,
|
||||||
|
ignore_conflicts=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
{"labels": LabelSerializer(labels, many=True).data},
|
||||||
|
status=status.HTTP_201_CREATED,
|
||||||
|
)
|
120
apiserver/plane/app/views/issue/link.py
Normal file
120
apiserver/plane/app/views/issue/link.py
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
# Python imports
|
||||||
|
import json
|
||||||
|
|
||||||
|
# Django imports
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.core.serializers.json import DjangoJSONEncoder
|
||||||
|
|
||||||
|
# Third Party imports
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework import status
|
||||||
|
|
||||||
|
# Module imports
|
||||||
|
from .. import BaseViewSet
|
||||||
|
from plane.app.serializers import IssueLinkSerializer
|
||||||
|
from plane.app.permissions import ProjectEntityPermission
|
||||||
|
from plane.db.models import IssueLink
|
||||||
|
from plane.bgtasks.issue_activites_task import issue_activity
|
||||||
|
|
||||||
|
|
||||||
|
class IssueLinkViewSet(BaseViewSet):
|
||||||
|
permission_classes = [
|
||||||
|
ProjectEntityPermission,
|
||||||
|
]
|
||||||
|
|
||||||
|
model = IssueLink
|
||||||
|
serializer_class = IssueLinkSerializer
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
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"))
|
||||||
|
.filter(
|
||||||
|
project__project_projectmember__member=self.request.user,
|
||||||
|
project__project_projectmember__is_active=True,
|
||||||
|
)
|
||||||
|
.order_by("-created_at")
|
||||||
|
.distinct()
|
||||||
|
)
|
||||||
|
|
||||||
|
def create(self, request, slug, project_id, issue_id):
|
||||||
|
serializer = IssueLinkSerializer(data=request.data)
|
||||||
|
if serializer.is_valid():
|
||||||
|
serializer.save(
|
||||||
|
project_id=project_id,
|
||||||
|
issue_id=issue_id,
|
||||||
|
)
|
||||||
|
issue_activity.delay(
|
||||||
|
type="link.activity.created",
|
||||||
|
requested_data=json.dumps(
|
||||||
|
serializer.data, cls=DjangoJSONEncoder
|
||||||
|
),
|
||||||
|
actor_id=str(self.request.user.id),
|
||||||
|
issue_id=str(self.kwargs.get("issue_id")),
|
||||||
|
project_id=str(self.kwargs.get("project_id")),
|
||||||
|
current_instance=None,
|
||||||
|
epoch=int(timezone.now().timestamp()),
|
||||||
|
notification=True,
|
||||||
|
origin=request.META.get("HTTP_ORIGIN"),
|
||||||
|
)
|
||||||
|
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):
|
||||||
|
issue_link = IssueLink.objects.get(
|
||||||
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
issue_id=issue_id,
|
||||||
|
pk=pk,
|
||||||
|
)
|
||||||
|
requested_data = json.dumps(request.data, cls=DjangoJSONEncoder)
|
||||||
|
current_instance = json.dumps(
|
||||||
|
IssueLinkSerializer(issue_link).data,
|
||||||
|
cls=DjangoJSONEncoder,
|
||||||
|
)
|
||||||
|
serializer = IssueLinkSerializer(
|
||||||
|
issue_link, data=request.data, partial=True
|
||||||
|
)
|
||||||
|
if serializer.is_valid():
|
||||||
|
serializer.save()
|
||||||
|
issue_activity.delay(
|
||||||
|
type="link.activity.updated",
|
||||||
|
requested_data=requested_data,
|
||||||
|
actor_id=str(request.user.id),
|
||||||
|
issue_id=str(issue_id),
|
||||||
|
project_id=str(project_id),
|
||||||
|
current_instance=current_instance,
|
||||||
|
epoch=int(timezone.now().timestamp()),
|
||||||
|
notification=True,
|
||||||
|
origin=request.META.get("HTTP_ORIGIN"),
|
||||||
|
)
|
||||||
|
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):
|
||||||
|
issue_link = IssueLink.objects.get(
|
||||||
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
issue_id=issue_id,
|
||||||
|
pk=pk,
|
||||||
|
)
|
||||||
|
current_instance = json.dumps(
|
||||||
|
IssueLinkSerializer(issue_link).data,
|
||||||
|
cls=DjangoJSONEncoder,
|
||||||
|
)
|
||||||
|
issue_activity.delay(
|
||||||
|
type="link.activity.deleted",
|
||||||
|
requested_data=json.dumps({"link_id": str(pk)}),
|
||||||
|
actor_id=str(request.user.id),
|
||||||
|
issue_id=str(issue_id),
|
||||||
|
project_id=str(project_id),
|
||||||
|
current_instance=current_instance,
|
||||||
|
epoch=int(timezone.now().timestamp()),
|
||||||
|
notification=True,
|
||||||
|
origin=request.META.get("HTTP_ORIGIN"),
|
||||||
|
)
|
||||||
|
issue_link.delete()
|
||||||
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
89
apiserver/plane/app/views/issue/reaction.py
Normal file
89
apiserver/plane/app/views/issue/reaction.py
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
# Python imports
|
||||||
|
import json
|
||||||
|
|
||||||
|
# Django imports
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.core.serializers.json import DjangoJSONEncoder
|
||||||
|
|
||||||
|
# Third Party imports
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework import status
|
||||||
|
|
||||||
|
# Module imports
|
||||||
|
from .. import BaseViewSet
|
||||||
|
from plane.app.serializers import IssueReactionSerializer
|
||||||
|
from plane.app.permissions import ProjectLitePermission
|
||||||
|
from plane.db.models import IssueReaction
|
||||||
|
from plane.bgtasks.issue_activites_task import issue_activity
|
||||||
|
|
||||||
|
|
||||||
|
class IssueReactionViewSet(BaseViewSet):
|
||||||
|
serializer_class = IssueReactionSerializer
|
||||||
|
model = IssueReaction
|
||||||
|
permission_classes = [
|
||||||
|
ProjectLitePermission,
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
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"))
|
||||||
|
.filter(
|
||||||
|
project__project_projectmember__member=self.request.user,
|
||||||
|
project__project_projectmember__is_active=True,
|
||||||
|
)
|
||||||
|
.order_by("-created_at")
|
||||||
|
.distinct()
|
||||||
|
)
|
||||||
|
|
||||||
|
def create(self, request, slug, project_id, issue_id):
|
||||||
|
serializer = IssueReactionSerializer(data=request.data)
|
||||||
|
if serializer.is_valid():
|
||||||
|
serializer.save(
|
||||||
|
issue_id=issue_id,
|
||||||
|
project_id=project_id,
|
||||||
|
actor=request.user,
|
||||||
|
)
|
||||||
|
issue_activity.delay(
|
||||||
|
type="issue_reaction.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()),
|
||||||
|
notification=True,
|
||||||
|
origin=request.META.get("HTTP_ORIGIN"),
|
||||||
|
)
|
||||||
|
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):
|
||||||
|
issue_reaction = IssueReaction.objects.get(
|
||||||
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
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()),
|
||||||
|
notification=True,
|
||||||
|
origin=request.META.get("HTTP_ORIGIN"),
|
||||||
|
)
|
||||||
|
issue_reaction.delete()
|
||||||
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
204
apiserver/plane/app/views/issue/relation.py
Normal file
204
apiserver/plane/app/views/issue/relation.py
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
# Python imports
|
||||||
|
import json
|
||||||
|
|
||||||
|
# Django imports
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.db.models import Q
|
||||||
|
from django.core.serializers.json import DjangoJSONEncoder
|
||||||
|
|
||||||
|
# Third Party imports
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework import status
|
||||||
|
|
||||||
|
# Module imports
|
||||||
|
from .. import BaseViewSet
|
||||||
|
from plane.app.serializers import (
|
||||||
|
IssueRelationSerializer,
|
||||||
|
RelatedIssueSerializer,
|
||||||
|
)
|
||||||
|
from plane.app.permissions import ProjectEntityPermission
|
||||||
|
from plane.db.models import (
|
||||||
|
Project,
|
||||||
|
IssueRelation,
|
||||||
|
)
|
||||||
|
from plane.bgtasks.issue_activites_task import issue_activity
|
||||||
|
|
||||||
|
|
||||||
|
class IssueRelationViewSet(BaseViewSet):
|
||||||
|
serializer_class = IssueRelationSerializer
|
||||||
|
model = IssueRelation
|
||||||
|
permission_classes = [
|
||||||
|
ProjectEntityPermission,
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return self.filter_queryset(
|
||||||
|
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"))
|
||||||
|
.filter(
|
||||||
|
project__project_projectmember__member=self.request.user,
|
||||||
|
project__project_projectmember__is_active=True,
|
||||||
|
)
|
||||||
|
.select_related("project")
|
||||||
|
.select_related("workspace")
|
||||||
|
.select_related("issue")
|
||||||
|
.distinct()
|
||||||
|
)
|
||||||
|
|
||||||
|
def list(self, request, slug, project_id, issue_id):
|
||||||
|
issue_relations = (
|
||||||
|
IssueRelation.objects.filter(
|
||||||
|
Q(issue_id=issue_id) | Q(related_issue=issue_id)
|
||||||
|
)
|
||||||
|
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||||
|
.select_related("project")
|
||||||
|
.select_related("workspace")
|
||||||
|
.select_related("issue")
|
||||||
|
.order_by("-created_at")
|
||||||
|
.distinct()
|
||||||
|
)
|
||||||
|
|
||||||
|
blocking_issues = issue_relations.filter(
|
||||||
|
relation_type="blocked_by", related_issue_id=issue_id
|
||||||
|
)
|
||||||
|
blocked_by_issues = issue_relations.filter(
|
||||||
|
relation_type="blocked_by", issue_id=issue_id
|
||||||
|
)
|
||||||
|
duplicate_issues = issue_relations.filter(
|
||||||
|
issue_id=issue_id, relation_type="duplicate"
|
||||||
|
)
|
||||||
|
duplicate_issues_related = issue_relations.filter(
|
||||||
|
related_issue_id=issue_id, relation_type="duplicate"
|
||||||
|
)
|
||||||
|
relates_to_issues = issue_relations.filter(
|
||||||
|
issue_id=issue_id, relation_type="relates_to"
|
||||||
|
)
|
||||||
|
relates_to_issues_related = issue_relations.filter(
|
||||||
|
related_issue_id=issue_id, relation_type="relates_to"
|
||||||
|
)
|
||||||
|
|
||||||
|
blocked_by_issues_serialized = IssueRelationSerializer(
|
||||||
|
blocked_by_issues, many=True
|
||||||
|
).data
|
||||||
|
duplicate_issues_serialized = IssueRelationSerializer(
|
||||||
|
duplicate_issues, many=True
|
||||||
|
).data
|
||||||
|
relates_to_issues_serialized = IssueRelationSerializer(
|
||||||
|
relates_to_issues, many=True
|
||||||
|
).data
|
||||||
|
|
||||||
|
# revere relation for blocked by issues
|
||||||
|
blocking_issues_serialized = RelatedIssueSerializer(
|
||||||
|
blocking_issues, many=True
|
||||||
|
).data
|
||||||
|
# reverse relation for duplicate issues
|
||||||
|
duplicate_issues_related_serialized = RelatedIssueSerializer(
|
||||||
|
duplicate_issues_related, many=True
|
||||||
|
).data
|
||||||
|
# reverse relation for related issues
|
||||||
|
relates_to_issues_related_serialized = RelatedIssueSerializer(
|
||||||
|
relates_to_issues_related, many=True
|
||||||
|
).data
|
||||||
|
|
||||||
|
response_data = {
|
||||||
|
"blocking": blocking_issues_serialized,
|
||||||
|
"blocked_by": blocked_by_issues_serialized,
|
||||||
|
"duplicate": duplicate_issues_serialized
|
||||||
|
+ duplicate_issues_related_serialized,
|
||||||
|
"relates_to": relates_to_issues_serialized
|
||||||
|
+ relates_to_issues_related_serialized,
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response(response_data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
def create(self, request, slug, project_id, issue_id):
|
||||||
|
relation_type = request.data.get("relation_type", None)
|
||||||
|
issues = request.data.get("issues", [])
|
||||||
|
project = Project.objects.get(pk=project_id)
|
||||||
|
|
||||||
|
issue_relation = IssueRelation.objects.bulk_create(
|
||||||
|
[
|
||||||
|
IssueRelation(
|
||||||
|
issue_id=(
|
||||||
|
issue if relation_type == "blocking" else issue_id
|
||||||
|
),
|
||||||
|
related_issue_id=(
|
||||||
|
issue_id if relation_type == "blocking" else issue
|
||||||
|
),
|
||||||
|
relation_type=(
|
||||||
|
"blocked_by"
|
||||||
|
if relation_type == "blocking"
|
||||||
|
else relation_type
|
||||||
|
),
|
||||||
|
project_id=project_id,
|
||||||
|
workspace_id=project.workspace_id,
|
||||||
|
created_by=request.user,
|
||||||
|
updated_by=request.user,
|
||||||
|
)
|
||||||
|
for issue in issues
|
||||||
|
],
|
||||||
|
batch_size=10,
|
||||||
|
ignore_conflicts=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
issue_activity.delay(
|
||||||
|
type="issue_relation.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()),
|
||||||
|
notification=True,
|
||||||
|
origin=request.META.get("HTTP_ORIGIN"),
|
||||||
|
)
|
||||||
|
|
||||||
|
if relation_type == "blocking":
|
||||||
|
return Response(
|
||||||
|
RelatedIssueSerializer(issue_relation, many=True).data,
|
||||||
|
status=status.HTTP_201_CREATED,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return Response(
|
||||||
|
IssueRelationSerializer(issue_relation, many=True).data,
|
||||||
|
status=status.HTTP_201_CREATED,
|
||||||
|
)
|
||||||
|
|
||||||
|
def remove_relation(self, request, slug, project_id, issue_id):
|
||||||
|
relation_type = request.data.get("relation_type", None)
|
||||||
|
related_issue = request.data.get("related_issue", None)
|
||||||
|
|
||||||
|
if relation_type == "blocking":
|
||||||
|
issue_relation = IssueRelation.objects.get(
|
||||||
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
issue_id=related_issue,
|
||||||
|
related_issue_id=issue_id,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
issue_relation = IssueRelation.objects.get(
|
||||||
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
issue_id=issue_id,
|
||||||
|
related_issue_id=related_issue,
|
||||||
|
)
|
||||||
|
current_instance = json.dumps(
|
||||||
|
IssueRelationSerializer(issue_relation).data,
|
||||||
|
cls=DjangoJSONEncoder,
|
||||||
|
)
|
||||||
|
issue_relation.delete()
|
||||||
|
issue_activity.delay(
|
||||||
|
type="issue_relation.activity.deleted",
|
||||||
|
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=current_instance,
|
||||||
|
epoch=int(timezone.now().timestamp()),
|
||||||
|
notification=True,
|
||||||
|
origin=request.META.get("HTTP_ORIGIN"),
|
||||||
|
)
|
||||||
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
195
apiserver/plane/app/views/issue/sub_issue.py
Normal file
195
apiserver/plane/app/views/issue/sub_issue.py
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
# Python imports
|
||||||
|
import json
|
||||||
|
|
||||||
|
# Django imports
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.db.models import (
|
||||||
|
OuterRef,
|
||||||
|
Func,
|
||||||
|
F,
|
||||||
|
Q,
|
||||||
|
Value,
|
||||||
|
UUIDField,
|
||||||
|
)
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
|
from django.views.decorators.gzip import gzip_page
|
||||||
|
from django.contrib.postgres.aggregates import ArrayAgg
|
||||||
|
from django.contrib.postgres.fields import ArrayField
|
||||||
|
from django.db.models.functions import Coalesce
|
||||||
|
|
||||||
|
# Third Party imports
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework import status
|
||||||
|
|
||||||
|
# Module imports
|
||||||
|
from .. import BaseAPIView
|
||||||
|
from plane.app.serializers import IssueSerializer
|
||||||
|
from plane.app.permissions import ProjectEntityPermission
|
||||||
|
from plane.db.models import (
|
||||||
|
Issue,
|
||||||
|
IssueLink,
|
||||||
|
IssueAttachment,
|
||||||
|
)
|
||||||
|
from plane.bgtasks.issue_activites_task import issue_activity
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
|
||||||
|
class SubIssuesEndpoint(BaseAPIView):
|
||||||
|
permission_classes = [
|
||||||
|
ProjectEntityPermission,
|
||||||
|
]
|
||||||
|
|
||||||
|
@method_decorator(gzip_page)
|
||||||
|
def get(self, request, slug, project_id, issue_id):
|
||||||
|
sub_issues = (
|
||||||
|
Issue.issue_objects.filter(
|
||||||
|
parent_id=issue_id, workspace__slug=slug
|
||||||
|
)
|
||||||
|
.select_related("workspace", "project", "state", "parent")
|
||||||
|
.prefetch_related("assignees", "labels", "issue_module__module")
|
||||||
|
.annotate(cycle_id=F("issue_cycle__cycle_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")
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
sub_issues_count=Issue.issue_objects.filter(
|
||||||
|
parent=OuterRef("id")
|
||||||
|
)
|
||||||
|
.order_by()
|
||||||
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
|
.values("count")
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
label_ids=Coalesce(
|
||||||
|
ArrayAgg(
|
||||||
|
"labels__id",
|
||||||
|
distinct=True,
|
||||||
|
filter=~Q(labels__id__isnull=True),
|
||||||
|
),
|
||||||
|
Value([], output_field=ArrayField(UUIDField())),
|
||||||
|
),
|
||||||
|
assignee_ids=Coalesce(
|
||||||
|
ArrayAgg(
|
||||||
|
"assignees__id",
|
||||||
|
distinct=True,
|
||||||
|
filter=~Q(assignees__id__isnull=True),
|
||||||
|
),
|
||||||
|
Value([], output_field=ArrayField(UUIDField())),
|
||||||
|
),
|
||||||
|
module_ids=Coalesce(
|
||||||
|
ArrayAgg(
|
||||||
|
"issue_module__module_id",
|
||||||
|
distinct=True,
|
||||||
|
filter=~Q(issue_module__module_id__isnull=True),
|
||||||
|
),
|
||||||
|
Value([], output_field=ArrayField(UUIDField())),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.annotate(state_group=F("state__group"))
|
||||||
|
)
|
||||||
|
|
||||||
|
# create's a dict with state group name with their respective issue id's
|
||||||
|
result = defaultdict(list)
|
||||||
|
for sub_issue in sub_issues:
|
||||||
|
result[sub_issue.state_group].append(str(sub_issue.id))
|
||||||
|
|
||||||
|
sub_issues = sub_issues.values(
|
||||||
|
"id",
|
||||||
|
"name",
|
||||||
|
"state_id",
|
||||||
|
"sort_order",
|
||||||
|
"completed_at",
|
||||||
|
"estimate_point",
|
||||||
|
"priority",
|
||||||
|
"start_date",
|
||||||
|
"target_date",
|
||||||
|
"sequence_id",
|
||||||
|
"project_id",
|
||||||
|
"parent_id",
|
||||||
|
"cycle_id",
|
||||||
|
"module_ids",
|
||||||
|
"label_ids",
|
||||||
|
"assignee_ids",
|
||||||
|
"sub_issues_count",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
"created_by",
|
||||||
|
"updated_by",
|
||||||
|
"attachment_count",
|
||||||
|
"link_count",
|
||||||
|
"is_draft",
|
||||||
|
"archived_at",
|
||||||
|
)
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"sub_issues": sub_issues,
|
||||||
|
"state_distribution": result,
|
||||||
|
},
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Assign multiple sub issues
|
||||||
|
def post(self, request, slug, project_id, issue_id):
|
||||||
|
parent_issue = Issue.issue_objects.get(pk=issue_id)
|
||||||
|
sub_issue_ids = request.data.get("sub_issue_ids", [])
|
||||||
|
|
||||||
|
if not len(sub_issue_ids):
|
||||||
|
return Response(
|
||||||
|
{"error": "Sub Issue IDs are required"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
sub_issues = Issue.issue_objects.filter(id__in=sub_issue_ids)
|
||||||
|
|
||||||
|
for sub_issue in sub_issues:
|
||||||
|
sub_issue.parent = parent_issue
|
||||||
|
|
||||||
|
_ = Issue.objects.bulk_update(sub_issues, ["parent"], batch_size=10)
|
||||||
|
|
||||||
|
updated_sub_issues = Issue.issue_objects.filter(
|
||||||
|
id__in=sub_issue_ids
|
||||||
|
).annotate(state_group=F("state__group"))
|
||||||
|
|
||||||
|
# Track the issue
|
||||||
|
_ = [
|
||||||
|
issue_activity.delay(
|
||||||
|
type="issue.activity.updated",
|
||||||
|
requested_data=json.dumps({"parent": str(issue_id)}),
|
||||||
|
actor_id=str(request.user.id),
|
||||||
|
issue_id=str(sub_issue_id),
|
||||||
|
project_id=str(project_id),
|
||||||
|
current_instance=json.dumps({"parent": str(sub_issue_id)}),
|
||||||
|
epoch=int(timezone.now().timestamp()),
|
||||||
|
notification=True,
|
||||||
|
origin=request.META.get("HTTP_ORIGIN"),
|
||||||
|
)
|
||||||
|
for sub_issue_id in sub_issue_ids
|
||||||
|
]
|
||||||
|
|
||||||
|
# create's a dict with state group name with their respective issue id's
|
||||||
|
result = defaultdict(list)
|
||||||
|
for sub_issue in updated_sub_issues:
|
||||||
|
result[sub_issue.state_group].append(str(sub_issue.id))
|
||||||
|
|
||||||
|
serializer = IssueSerializer(
|
||||||
|
updated_sub_issues,
|
||||||
|
many=True,
|
||||||
|
)
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"sub_issues": serializer.data,
|
||||||
|
"state_distribution": result,
|
||||||
|
},
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
124
apiserver/plane/app/views/issue/subscriber.py
Normal file
124
apiserver/plane/app/views/issue/subscriber.py
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
# Third Party imports
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework import status
|
||||||
|
|
||||||
|
# Module imports
|
||||||
|
from .. import BaseViewSet
|
||||||
|
from plane.app.serializers import (
|
||||||
|
IssueSubscriberSerializer,
|
||||||
|
ProjectMemberLiteSerializer,
|
||||||
|
)
|
||||||
|
from plane.app.permissions import (
|
||||||
|
ProjectEntityPermission,
|
||||||
|
ProjectLitePermission,
|
||||||
|
)
|
||||||
|
from plane.db.models import (
|
||||||
|
IssueSubscriber,
|
||||||
|
ProjectMember,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class IssueSubscriberViewSet(BaseViewSet):
|
||||||
|
serializer_class = IssueSubscriberSerializer
|
||||||
|
model = IssueSubscriber
|
||||||
|
|
||||||
|
permission_classes = [
|
||||||
|
ProjectEntityPermission,
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_permissions(self):
|
||||||
|
if self.action in ["subscribe", "unsubscribe", "subscription_status"]:
|
||||||
|
self.permission_classes = [
|
||||||
|
ProjectLitePermission,
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
self.permission_classes = [
|
||||||
|
ProjectEntityPermission,
|
||||||
|
]
|
||||||
|
|
||||||
|
return super(IssueSubscriberViewSet, self).get_permissions()
|
||||||
|
|
||||||
|
def perform_create(self, serializer):
|
||||||
|
serializer.save(
|
||||||
|
project_id=self.kwargs.get("project_id"),
|
||||||
|
issue_id=self.kwargs.get("issue_id"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
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"))
|
||||||
|
.filter(
|
||||||
|
project__project_projectmember__member=self.request.user,
|
||||||
|
project__project_projectmember__is_active=True,
|
||||||
|
)
|
||||||
|
.order_by("-created_at")
|
||||||
|
.distinct()
|
||||||
|
)
|
||||||
|
|
||||||
|
def list(self, request, slug, project_id, issue_id):
|
||||||
|
members = ProjectMember.objects.filter(
|
||||||
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
is_active=True,
|
||||||
|
).select_related("member")
|
||||||
|
serializer = ProjectMemberLiteSerializer(members, many=True)
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
def destroy(self, request, slug, project_id, issue_id, subscriber_id):
|
||||||
|
issue_subscriber = IssueSubscriber.objects.get(
|
||||||
|
project=project_id,
|
||||||
|
subscriber=subscriber_id,
|
||||||
|
workspace__slug=slug,
|
||||||
|
issue=issue_id,
|
||||||
|
)
|
||||||
|
issue_subscriber.delete()
|
||||||
|
return Response(
|
||||||
|
status=status.HTTP_204_NO_CONTENT,
|
||||||
|
)
|
||||||
|
|
||||||
|
def subscribe(self, request, slug, project_id, issue_id):
|
||||||
|
if IssueSubscriber.objects.filter(
|
||||||
|
issue_id=issue_id,
|
||||||
|
subscriber=request.user,
|
||||||
|
workspace__slug=slug,
|
||||||
|
project=project_id,
|
||||||
|
).exists():
|
||||||
|
return Response(
|
||||||
|
{"message": "User already subscribed to the issue."},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
subscriber = IssueSubscriber.objects.create(
|
||||||
|
issue_id=issue_id,
|
||||||
|
subscriber_id=request.user.id,
|
||||||
|
project_id=project_id,
|
||||||
|
)
|
||||||
|
serializer = IssueSubscriberSerializer(subscriber)
|
||||||
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
|
def unsubscribe(self, request, slug, project_id, issue_id):
|
||||||
|
issue_subscriber = IssueSubscriber.objects.get(
|
||||||
|
project=project_id,
|
||||||
|
subscriber=request.user,
|
||||||
|
workspace__slug=slug,
|
||||||
|
issue=issue_id,
|
||||||
|
)
|
||||||
|
issue_subscriber.delete()
|
||||||
|
return Response(
|
||||||
|
status=status.HTTP_204_NO_CONTENT,
|
||||||
|
)
|
||||||
|
|
||||||
|
def subscription_status(self, request, slug, project_id, issue_id):
|
||||||
|
issue_subscriber = IssueSubscriber.objects.filter(
|
||||||
|
issue=issue_id,
|
||||||
|
subscriber=request.user,
|
||||||
|
workspace__slug=slug,
|
||||||
|
project=project_id,
|
||||||
|
).exists()
|
||||||
|
return Response(
|
||||||
|
{"subscribed": issue_subscriber}, status=status.HTTP_200_OK
|
||||||
|
)
|
@ -3,9 +3,7 @@ import json
|
|||||||
|
|
||||||
# Django Imports
|
# Django Imports
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.db.models import Prefetch, F, OuterRef, Func, Exists, Count, Q
|
from django.db.models import Prefetch, F, OuterRef, Exists, Count, Q
|
||||||
from django.utils.decorators import method_decorator
|
|
||||||
from django.views.decorators.gzip import gzip_page
|
|
||||||
from django.contrib.postgres.aggregates import ArrayAgg
|
from django.contrib.postgres.aggregates import ArrayAgg
|
||||||
from django.contrib.postgres.fields import ArrayField
|
from django.contrib.postgres.fields import ArrayField
|
||||||
from django.db.models import Value, UUIDField
|
from django.db.models import Value, UUIDField
|
||||||
@ -16,14 +14,12 @@ from rest_framework.response import Response
|
|||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from . import BaseViewSet, BaseAPIView, WebhookMixin
|
from .. import BaseViewSet, BaseAPIView, WebhookMixin
|
||||||
from plane.app.serializers import (
|
from plane.app.serializers import (
|
||||||
ModuleWriteSerializer,
|
ModuleWriteSerializer,
|
||||||
ModuleSerializer,
|
ModuleSerializer,
|
||||||
ModuleIssueSerializer,
|
|
||||||
ModuleLinkSerializer,
|
ModuleLinkSerializer,
|
||||||
ModuleFavoriteSerializer,
|
ModuleFavoriteSerializer,
|
||||||
IssueSerializer,
|
|
||||||
ModuleUserPropertiesSerializer,
|
ModuleUserPropertiesSerializer,
|
||||||
ModuleDetailSerializer,
|
ModuleDetailSerializer,
|
||||||
)
|
)
|
||||||
@ -38,12 +34,9 @@ from plane.db.models import (
|
|||||||
Issue,
|
Issue,
|
||||||
ModuleLink,
|
ModuleLink,
|
||||||
ModuleFavorite,
|
ModuleFavorite,
|
||||||
IssueLink,
|
|
||||||
IssueAttachment,
|
|
||||||
ModuleUserProperties,
|
ModuleUserProperties,
|
||||||
)
|
)
|
||||||
from plane.bgtasks.issue_activites_task import issue_activity
|
from plane.bgtasks.issue_activites_task import issue_activity
|
||||||
from plane.utils.issue_filters import issue_filters
|
|
||||||
from plane.utils.analytics_plot import burndown_plot
|
from plane.utils.analytics_plot import burndown_plot
|
||||||
|
|
||||||
|
|
||||||
@ -426,232 +419,6 @@ class ModuleViewSet(WebhookMixin, BaseViewSet):
|
|||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
|
||||||
class ModuleIssueViewSet(WebhookMixin, BaseViewSet):
|
|
||||||
serializer_class = ModuleIssueSerializer
|
|
||||||
model = ModuleIssue
|
|
||||||
webhook_event = "module_issue"
|
|
||||||
bulk = True
|
|
||||||
|
|
||||||
filterset_fields = [
|
|
||||||
"issue__labels__id",
|
|
||||||
"issue__assignees__id",
|
|
||||||
]
|
|
||||||
|
|
||||||
permission_classes = [
|
|
||||||
ProjectEntityPermission,
|
|
||||||
]
|
|
||||||
|
|
||||||
def get_queryset(self):
|
|
||||||
return (
|
|
||||||
Issue.issue_objects.filter(
|
|
||||||
project_id=self.kwargs.get("project_id"),
|
|
||||||
workspace__slug=self.kwargs.get("slug"),
|
|
||||||
issue_module__module_id=self.kwargs.get("module_id"),
|
|
||||||
)
|
|
||||||
.select_related("workspace", "project", "state", "parent")
|
|
||||||
.prefetch_related("assignees", "labels", "issue_module__module")
|
|
||||||
.annotate(cycle_id=F("issue_cycle__cycle_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")
|
|
||||||
)
|
|
||||||
.annotate(
|
|
||||||
sub_issues_count=Issue.issue_objects.filter(
|
|
||||||
parent=OuterRef("id")
|
|
||||||
)
|
|
||||||
.order_by()
|
|
||||||
.annotate(count=Func(F("id"), function="Count"))
|
|
||||||
.values("count")
|
|
||||||
)
|
|
||||||
.annotate(
|
|
||||||
label_ids=Coalesce(
|
|
||||||
ArrayAgg(
|
|
||||||
"labels__id",
|
|
||||||
distinct=True,
|
|
||||||
filter=~Q(labels__id__isnull=True),
|
|
||||||
),
|
|
||||||
Value([], output_field=ArrayField(UUIDField())),
|
|
||||||
),
|
|
||||||
assignee_ids=Coalesce(
|
|
||||||
ArrayAgg(
|
|
||||||
"assignees__id",
|
|
||||||
distinct=True,
|
|
||||||
filter=~Q(assignees__id__isnull=True),
|
|
||||||
),
|
|
||||||
Value([], output_field=ArrayField(UUIDField())),
|
|
||||||
),
|
|
||||||
module_ids=Coalesce(
|
|
||||||
ArrayAgg(
|
|
||||||
"issue_module__module_id",
|
|
||||||
distinct=True,
|
|
||||||
filter=~Q(issue_module__module_id__isnull=True),
|
|
||||||
),
|
|
||||||
Value([], output_field=ArrayField(UUIDField())),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
).distinct()
|
|
||||||
|
|
||||||
@method_decorator(gzip_page)
|
|
||||||
def list(self, request, slug, project_id, module_id):
|
|
||||||
fields = [
|
|
||||||
field
|
|
||||||
for field in request.GET.get("fields", "").split(",")
|
|
||||||
if field
|
|
||||||
]
|
|
||||||
filters = issue_filters(request.query_params, "GET")
|
|
||||||
issue_queryset = self.get_queryset().filter(**filters)
|
|
||||||
if self.fields or self.expand:
|
|
||||||
issues = IssueSerializer(
|
|
||||||
issue_queryset, many=True, fields=fields if fields else None
|
|
||||||
).data
|
|
||||||
else:
|
|
||||||
issues = issue_queryset.values(
|
|
||||||
"id",
|
|
||||||
"name",
|
|
||||||
"state_id",
|
|
||||||
"sort_order",
|
|
||||||
"completed_at",
|
|
||||||
"estimate_point",
|
|
||||||
"priority",
|
|
||||||
"start_date",
|
|
||||||
"target_date",
|
|
||||||
"sequence_id",
|
|
||||||
"project_id",
|
|
||||||
"parent_id",
|
|
||||||
"cycle_id",
|
|
||||||
"module_ids",
|
|
||||||
"label_ids",
|
|
||||||
"assignee_ids",
|
|
||||||
"sub_issues_count",
|
|
||||||
"created_at",
|
|
||||||
"updated_at",
|
|
||||||
"created_by",
|
|
||||||
"updated_by",
|
|
||||||
"attachment_count",
|
|
||||||
"link_count",
|
|
||||||
"is_draft",
|
|
||||||
"archived_at",
|
|
||||||
)
|
|
||||||
return Response(issues, status=status.HTTP_200_OK)
|
|
||||||
|
|
||||||
# create multiple issues inside a module
|
|
||||||
def create_module_issues(self, request, slug, project_id, module_id):
|
|
||||||
issues = request.data.get("issues", [])
|
|
||||||
if not issues:
|
|
||||||
return Response(
|
|
||||||
{"error": "Issues are required"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
project = Project.objects.get(pk=project_id)
|
|
||||||
_ = ModuleIssue.objects.bulk_create(
|
|
||||||
[
|
|
||||||
ModuleIssue(
|
|
||||||
issue_id=str(issue),
|
|
||||||
module_id=module_id,
|
|
||||||
project_id=project_id,
|
|
||||||
workspace_id=project.workspace_id,
|
|
||||||
created_by=request.user,
|
|
||||||
updated_by=request.user,
|
|
||||||
)
|
|
||||||
for issue in issues
|
|
||||||
],
|
|
||||||
batch_size=10,
|
|
||||||
ignore_conflicts=True,
|
|
||||||
)
|
|
||||||
# Bulk Update the activity
|
|
||||||
_ = [
|
|
||||||
issue_activity.delay(
|
|
||||||
type="module.activity.created",
|
|
||||||
requested_data=json.dumps({"module_id": str(module_id)}),
|
|
||||||
actor_id=str(request.user.id),
|
|
||||||
issue_id=str(issue),
|
|
||||||
project_id=project_id,
|
|
||||||
current_instance=None,
|
|
||||||
epoch=int(timezone.now().timestamp()),
|
|
||||||
notification=True,
|
|
||||||
origin=request.META.get("HTTP_ORIGIN"),
|
|
||||||
)
|
|
||||||
for issue in issues
|
|
||||||
]
|
|
||||||
return Response({"message": "success"}, status=status.HTTP_201_CREATED)
|
|
||||||
|
|
||||||
# create multiple module inside an issue
|
|
||||||
def create_issue_modules(self, request, slug, project_id, issue_id):
|
|
||||||
modules = request.data.get("modules", [])
|
|
||||||
if not modules:
|
|
||||||
return Response(
|
|
||||||
{"error": "Modules are required"},
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
)
|
|
||||||
|
|
||||||
project = Project.objects.get(pk=project_id)
|
|
||||||
_ = ModuleIssue.objects.bulk_create(
|
|
||||||
[
|
|
||||||
ModuleIssue(
|
|
||||||
issue_id=issue_id,
|
|
||||||
module_id=module,
|
|
||||||
project_id=project_id,
|
|
||||||
workspace_id=project.workspace_id,
|
|
||||||
created_by=request.user,
|
|
||||||
updated_by=request.user,
|
|
||||||
)
|
|
||||||
for module in modules
|
|
||||||
],
|
|
||||||
batch_size=10,
|
|
||||||
ignore_conflicts=True,
|
|
||||||
)
|
|
||||||
# Bulk Update the activity
|
|
||||||
_ = [
|
|
||||||
issue_activity.delay(
|
|
||||||
type="module.activity.created",
|
|
||||||
requested_data=json.dumps({"module_id": module}),
|
|
||||||
actor_id=str(request.user.id),
|
|
||||||
issue_id=issue_id,
|
|
||||||
project_id=project_id,
|
|
||||||
current_instance=None,
|
|
||||||
epoch=int(timezone.now().timestamp()),
|
|
||||||
notification=True,
|
|
||||||
origin=request.META.get("HTTP_ORIGIN"),
|
|
||||||
)
|
|
||||||
for module in modules
|
|
||||||
]
|
|
||||||
|
|
||||||
return Response({"message": "success"}, status=status.HTTP_201_CREATED)
|
|
||||||
|
|
||||||
def destroy(self, request, slug, project_id, module_id, issue_id):
|
|
||||||
module_issue = ModuleIssue.objects.get(
|
|
||||||
workspace__slug=slug,
|
|
||||||
project_id=project_id,
|
|
||||||
module_id=module_id,
|
|
||||||
issue_id=issue_id,
|
|
||||||
)
|
|
||||||
issue_activity.delay(
|
|
||||||
type="module.activity.deleted",
|
|
||||||
requested_data=json.dumps({"module_id": str(module_id)}),
|
|
||||||
actor_id=str(request.user.id),
|
|
||||||
issue_id=str(issue_id),
|
|
||||||
project_id=str(project_id),
|
|
||||||
current_instance=json.dumps(
|
|
||||||
{"module_name": module_issue.module.name}
|
|
||||||
),
|
|
||||||
epoch=int(timezone.now().timestamp()),
|
|
||||||
notification=True,
|
|
||||||
origin=request.META.get("HTTP_ORIGIN"),
|
|
||||||
)
|
|
||||||
module_issue.delete()
|
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
|
||||||
|
|
||||||
|
|
||||||
class ModuleLinkViewSet(BaseViewSet):
|
class ModuleLinkViewSet(BaseViewSet):
|
||||||
permission_classes = [
|
permission_classes = [
|
||||||
ProjectEntityPermission,
|
ProjectEntityPermission,
|
259
apiserver/plane/app/views/module/issue.py
Normal file
259
apiserver/plane/app/views/module/issue.py
Normal file
@ -0,0 +1,259 @@
|
|||||||
|
# Python imports
|
||||||
|
import json
|
||||||
|
|
||||||
|
# Django Imports
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.db.models import F, OuterRef, Func, Q
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
|
from django.views.decorators.gzip import gzip_page
|
||||||
|
from django.contrib.postgres.aggregates import ArrayAgg
|
||||||
|
from django.contrib.postgres.fields import ArrayField
|
||||||
|
from django.db.models import Value, UUIDField
|
||||||
|
from django.db.models.functions import Coalesce
|
||||||
|
|
||||||
|
# Third party imports
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework import status
|
||||||
|
|
||||||
|
# Module imports
|
||||||
|
from .. import BaseViewSet, WebhookMixin
|
||||||
|
from plane.app.serializers import (
|
||||||
|
ModuleIssueSerializer,
|
||||||
|
IssueSerializer,
|
||||||
|
)
|
||||||
|
from plane.app.permissions import ProjectEntityPermission
|
||||||
|
from plane.db.models import (
|
||||||
|
ModuleIssue,
|
||||||
|
Project,
|
||||||
|
Issue,
|
||||||
|
IssueLink,
|
||||||
|
IssueAttachment,
|
||||||
|
)
|
||||||
|
from plane.bgtasks.issue_activites_task import issue_activity
|
||||||
|
from plane.utils.issue_filters import issue_filters
|
||||||
|
|
||||||
|
|
||||||
|
class ModuleIssueViewSet(WebhookMixin, BaseViewSet):
|
||||||
|
serializer_class = ModuleIssueSerializer
|
||||||
|
model = ModuleIssue
|
||||||
|
webhook_event = "module_issue"
|
||||||
|
bulk = True
|
||||||
|
|
||||||
|
filterset_fields = [
|
||||||
|
"issue__labels__id",
|
||||||
|
"issue__assignees__id",
|
||||||
|
]
|
||||||
|
|
||||||
|
permission_classes = [
|
||||||
|
ProjectEntityPermission,
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return (
|
||||||
|
Issue.issue_objects.filter(
|
||||||
|
project_id=self.kwargs.get("project_id"),
|
||||||
|
workspace__slug=self.kwargs.get("slug"),
|
||||||
|
issue_module__module_id=self.kwargs.get("module_id"),
|
||||||
|
)
|
||||||
|
.select_related("workspace", "project", "state", "parent")
|
||||||
|
.prefetch_related("assignees", "labels", "issue_module__module")
|
||||||
|
.annotate(cycle_id=F("issue_cycle__cycle_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")
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
sub_issues_count=Issue.issue_objects.filter(
|
||||||
|
parent=OuterRef("id")
|
||||||
|
)
|
||||||
|
.order_by()
|
||||||
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
|
.values("count")
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
label_ids=Coalesce(
|
||||||
|
ArrayAgg(
|
||||||
|
"labels__id",
|
||||||
|
distinct=True,
|
||||||
|
filter=~Q(labels__id__isnull=True),
|
||||||
|
),
|
||||||
|
Value([], output_field=ArrayField(UUIDField())),
|
||||||
|
),
|
||||||
|
assignee_ids=Coalesce(
|
||||||
|
ArrayAgg(
|
||||||
|
"assignees__id",
|
||||||
|
distinct=True,
|
||||||
|
filter=~Q(assignees__id__isnull=True),
|
||||||
|
),
|
||||||
|
Value([], output_field=ArrayField(UUIDField())),
|
||||||
|
),
|
||||||
|
module_ids=Coalesce(
|
||||||
|
ArrayAgg(
|
||||||
|
"issue_module__module_id",
|
||||||
|
distinct=True,
|
||||||
|
filter=~Q(issue_module__module_id__isnull=True),
|
||||||
|
),
|
||||||
|
Value([], output_field=ArrayField(UUIDField())),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
).distinct()
|
||||||
|
|
||||||
|
@method_decorator(gzip_page)
|
||||||
|
def list(self, request, slug, project_id, module_id):
|
||||||
|
fields = [
|
||||||
|
field
|
||||||
|
for field in request.GET.get("fields", "").split(",")
|
||||||
|
if field
|
||||||
|
]
|
||||||
|
filters = issue_filters(request.query_params, "GET")
|
||||||
|
issue_queryset = self.get_queryset().filter(**filters)
|
||||||
|
if self.fields or self.expand:
|
||||||
|
issues = IssueSerializer(
|
||||||
|
issue_queryset, many=True, fields=fields if fields else None
|
||||||
|
).data
|
||||||
|
else:
|
||||||
|
issues = issue_queryset.values(
|
||||||
|
"id",
|
||||||
|
"name",
|
||||||
|
"state_id",
|
||||||
|
"sort_order",
|
||||||
|
"completed_at",
|
||||||
|
"estimate_point",
|
||||||
|
"priority",
|
||||||
|
"start_date",
|
||||||
|
"target_date",
|
||||||
|
"sequence_id",
|
||||||
|
"project_id",
|
||||||
|
"parent_id",
|
||||||
|
"cycle_id",
|
||||||
|
"module_ids",
|
||||||
|
"label_ids",
|
||||||
|
"assignee_ids",
|
||||||
|
"sub_issues_count",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
"created_by",
|
||||||
|
"updated_by",
|
||||||
|
"attachment_count",
|
||||||
|
"link_count",
|
||||||
|
"is_draft",
|
||||||
|
"archived_at",
|
||||||
|
)
|
||||||
|
return Response(issues, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
# create multiple issues inside a module
|
||||||
|
def create_module_issues(self, request, slug, project_id, module_id):
|
||||||
|
issues = request.data.get("issues", [])
|
||||||
|
if not issues:
|
||||||
|
return Response(
|
||||||
|
{"error": "Issues are required"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
project = Project.objects.get(pk=project_id)
|
||||||
|
_ = ModuleIssue.objects.bulk_create(
|
||||||
|
[
|
||||||
|
ModuleIssue(
|
||||||
|
issue_id=str(issue),
|
||||||
|
module_id=module_id,
|
||||||
|
project_id=project_id,
|
||||||
|
workspace_id=project.workspace_id,
|
||||||
|
created_by=request.user,
|
||||||
|
updated_by=request.user,
|
||||||
|
)
|
||||||
|
for issue in issues
|
||||||
|
],
|
||||||
|
batch_size=10,
|
||||||
|
ignore_conflicts=True,
|
||||||
|
)
|
||||||
|
# Bulk Update the activity
|
||||||
|
_ = [
|
||||||
|
issue_activity.delay(
|
||||||
|
type="module.activity.created",
|
||||||
|
requested_data=json.dumps({"module_id": str(module_id)}),
|
||||||
|
actor_id=str(request.user.id),
|
||||||
|
issue_id=str(issue),
|
||||||
|
project_id=project_id,
|
||||||
|
current_instance=None,
|
||||||
|
epoch=int(timezone.now().timestamp()),
|
||||||
|
notification=True,
|
||||||
|
origin=request.META.get("HTTP_ORIGIN"),
|
||||||
|
)
|
||||||
|
for issue in issues
|
||||||
|
]
|
||||||
|
return Response({"message": "success"}, status=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
|
# create multiple module inside an issue
|
||||||
|
def create_issue_modules(self, request, slug, project_id, issue_id):
|
||||||
|
modules = request.data.get("modules", [])
|
||||||
|
if not modules:
|
||||||
|
return Response(
|
||||||
|
{"error": "Modules are required"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
project = Project.objects.get(pk=project_id)
|
||||||
|
_ = ModuleIssue.objects.bulk_create(
|
||||||
|
[
|
||||||
|
ModuleIssue(
|
||||||
|
issue_id=issue_id,
|
||||||
|
module_id=module,
|
||||||
|
project_id=project_id,
|
||||||
|
workspace_id=project.workspace_id,
|
||||||
|
created_by=request.user,
|
||||||
|
updated_by=request.user,
|
||||||
|
)
|
||||||
|
for module in modules
|
||||||
|
],
|
||||||
|
batch_size=10,
|
||||||
|
ignore_conflicts=True,
|
||||||
|
)
|
||||||
|
# Bulk Update the activity
|
||||||
|
_ = [
|
||||||
|
issue_activity.delay(
|
||||||
|
type="module.activity.created",
|
||||||
|
requested_data=json.dumps({"module_id": module}),
|
||||||
|
actor_id=str(request.user.id),
|
||||||
|
issue_id=issue_id,
|
||||||
|
project_id=project_id,
|
||||||
|
current_instance=None,
|
||||||
|
epoch=int(timezone.now().timestamp()),
|
||||||
|
notification=True,
|
||||||
|
origin=request.META.get("HTTP_ORIGIN"),
|
||||||
|
)
|
||||||
|
for module in modules
|
||||||
|
]
|
||||||
|
|
||||||
|
return Response({"message": "success"}, status=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
|
def destroy(self, request, slug, project_id, module_id, issue_id):
|
||||||
|
module_issue = ModuleIssue.objects.get(
|
||||||
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
module_id=module_id,
|
||||||
|
issue_id=issue_id,
|
||||||
|
)
|
||||||
|
issue_activity.delay(
|
||||||
|
type="module.activity.deleted",
|
||||||
|
requested_data=json.dumps({"module_id": str(module_id)}),
|
||||||
|
actor_id=str(request.user.id),
|
||||||
|
issue_id=str(issue_id),
|
||||||
|
project_id=str(project_id),
|
||||||
|
current_instance=json.dumps(
|
||||||
|
{"module_name": module_issue.module.name}
|
||||||
|
),
|
||||||
|
epoch=int(timezone.now().timestamp()),
|
||||||
|
notification=True,
|
||||||
|
origin=request.META.get("HTTP_ORIGIN"),
|
||||||
|
)
|
||||||
|
module_issue.delete()
|
||||||
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
@ -8,7 +8,7 @@ from rest_framework.response import Response
|
|||||||
from plane.utils.paginator import BasePaginator
|
from plane.utils.paginator import BasePaginator
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from .base import BaseViewSet, BaseAPIView
|
from ..base import BaseViewSet, BaseAPIView
|
||||||
from plane.db.models import (
|
from plane.db.models import (
|
||||||
Notification,
|
Notification,
|
||||||
IssueAssignee,
|
IssueAssignee,
|
@ -26,7 +26,7 @@ from plane.db.models import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from .base import BaseAPIView, BaseViewSet
|
from ..base import BaseAPIView, BaseViewSet
|
||||||
|
|
||||||
|
|
||||||
def unarchive_archive_page_and_descendants(page_id, archived_at):
|
def unarchive_archive_page_and_descendants(page_id, archived_at):
|
File diff suppressed because it is too large
Load Diff
549
apiserver/plane/app/views/project/base.py
Normal file
549
apiserver/plane/app/views/project/base.py
Normal file
@ -0,0 +1,549 @@
|
|||||||
|
# Python imports
|
||||||
|
import boto3
|
||||||
|
|
||||||
|
# Django imports
|
||||||
|
from django.db import IntegrityError
|
||||||
|
from django.db.models import (
|
||||||
|
Prefetch,
|
||||||
|
Q,
|
||||||
|
Exists,
|
||||||
|
OuterRef,
|
||||||
|
F,
|
||||||
|
Func,
|
||||||
|
Subquery,
|
||||||
|
)
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
# Third Party imports
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework import serializers
|
||||||
|
from rest_framework.permissions import AllowAny
|
||||||
|
|
||||||
|
# Module imports
|
||||||
|
from plane.app.views.base import BaseViewSet, BaseAPIView, WebhookMixin
|
||||||
|
from plane.app.serializers import (
|
||||||
|
ProjectSerializer,
|
||||||
|
ProjectListSerializer,
|
||||||
|
ProjectFavoriteSerializer,
|
||||||
|
ProjectDeployBoardSerializer,
|
||||||
|
)
|
||||||
|
|
||||||
|
from plane.app.permissions import (
|
||||||
|
ProjectBasePermission,
|
||||||
|
ProjectMemberPermission,
|
||||||
|
)
|
||||||
|
|
||||||
|
from plane.db.models import (
|
||||||
|
Project,
|
||||||
|
ProjectMember,
|
||||||
|
Workspace,
|
||||||
|
State,
|
||||||
|
ProjectFavorite,
|
||||||
|
ProjectIdentifier,
|
||||||
|
Module,
|
||||||
|
Cycle,
|
||||||
|
Inbox,
|
||||||
|
ProjectDeployBoard,
|
||||||
|
IssueProperty,
|
||||||
|
)
|
||||||
|
from plane.utils.cache import cache_response
|
||||||
|
|
||||||
|
class ProjectViewSet(WebhookMixin, BaseViewSet):
|
||||||
|
serializer_class = ProjectListSerializer
|
||||||
|
model = Project
|
||||||
|
webhook_event = "project"
|
||||||
|
|
||||||
|
permission_classes = [
|
||||||
|
ProjectBasePermission,
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
sort_order = ProjectMember.objects.filter(
|
||||||
|
member=self.request.user,
|
||||||
|
project_id=OuterRef("pk"),
|
||||||
|
workspace__slug=self.kwargs.get("slug"),
|
||||||
|
is_active=True,
|
||||||
|
).values("sort_order")
|
||||||
|
return self.filter_queryset(
|
||||||
|
super()
|
||||||
|
.get_queryset()
|
||||||
|
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||||
|
.filter(
|
||||||
|
Q(project_projectmember__member=self.request.user)
|
||||||
|
| Q(network=2)
|
||||||
|
)
|
||||||
|
.select_related(
|
||||||
|
"workspace",
|
||||||
|
"workspace__owner",
|
||||||
|
"default_assignee",
|
||||||
|
"project_lead",
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
is_favorite=Exists(
|
||||||
|
ProjectFavorite.objects.filter(
|
||||||
|
user=self.request.user,
|
||||||
|
project_id=OuterRef("pk"),
|
||||||
|
workspace__slug=self.kwargs.get("slug"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
is_member=Exists(
|
||||||
|
ProjectMember.objects.filter(
|
||||||
|
member=self.request.user,
|
||||||
|
project_id=OuterRef("pk"),
|
||||||
|
workspace__slug=self.kwargs.get("slug"),
|
||||||
|
is_active=True,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
total_members=ProjectMember.objects.filter(
|
||||||
|
project_id=OuterRef("id"),
|
||||||
|
member__is_bot=False,
|
||||||
|
is_active=True,
|
||||||
|
)
|
||||||
|
.order_by()
|
||||||
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
|
.values("count")
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
total_cycles=Cycle.objects.filter(project_id=OuterRef("id"))
|
||||||
|
.order_by()
|
||||||
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
|
.values("count")
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
total_modules=Module.objects.filter(project_id=OuterRef("id"))
|
||||||
|
.order_by()
|
||||||
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
|
.values("count")
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
member_role=ProjectMember.objects.filter(
|
||||||
|
project_id=OuterRef("pk"),
|
||||||
|
member_id=self.request.user.id,
|
||||||
|
is_active=True,
|
||||||
|
).values("role")
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
is_deployed=Exists(
|
||||||
|
ProjectDeployBoard.objects.filter(
|
||||||
|
project_id=OuterRef("pk"),
|
||||||
|
workspace__slug=self.kwargs.get("slug"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.annotate(sort_order=Subquery(sort_order))
|
||||||
|
.prefetch_related(
|
||||||
|
Prefetch(
|
||||||
|
"project_projectmember",
|
||||||
|
queryset=ProjectMember.objects.filter(
|
||||||
|
workspace__slug=self.kwargs.get("slug"),
|
||||||
|
is_active=True,
|
||||||
|
).select_related("member"),
|
||||||
|
to_attr="members_list",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.distinct()
|
||||||
|
)
|
||||||
|
|
||||||
|
def list(self, request, slug):
|
||||||
|
fields = [
|
||||||
|
field
|
||||||
|
for field in request.GET.get("fields", "").split(",")
|
||||||
|
if field
|
||||||
|
]
|
||||||
|
projects = self.get_queryset().order_by("sort_order", "name")
|
||||||
|
if request.GET.get("per_page", False) and request.GET.get(
|
||||||
|
"cursor", False
|
||||||
|
):
|
||||||
|
return self.paginate(
|
||||||
|
request=request,
|
||||||
|
queryset=(projects),
|
||||||
|
on_results=lambda projects: ProjectListSerializer(
|
||||||
|
projects, many=True
|
||||||
|
).data,
|
||||||
|
)
|
||||||
|
projects = ProjectListSerializer(
|
||||||
|
projects, many=True, fields=fields if fields else None
|
||||||
|
).data
|
||||||
|
return Response(projects, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
def create(self, request, slug):
|
||||||
|
try:
|
||||||
|
workspace = Workspace.objects.get(slug=slug)
|
||||||
|
|
||||||
|
serializer = ProjectSerializer(
|
||||||
|
data={**request.data}, context={"workspace_id": workspace.id}
|
||||||
|
)
|
||||||
|
if serializer.is_valid():
|
||||||
|
serializer.save()
|
||||||
|
|
||||||
|
# Add the user as Administrator to the project
|
||||||
|
_ = ProjectMember.objects.create(
|
||||||
|
project_id=serializer.data["id"],
|
||||||
|
member=request.user,
|
||||||
|
role=20,
|
||||||
|
)
|
||||||
|
# Also create the issue property for the user
|
||||||
|
_ = IssueProperty.objects.create(
|
||||||
|
project_id=serializer.data["id"],
|
||||||
|
user=request.user,
|
||||||
|
)
|
||||||
|
|
||||||
|
if serializer.data["project_lead"] is not None and str(
|
||||||
|
serializer.data["project_lead"]
|
||||||
|
) != str(request.user.id):
|
||||||
|
ProjectMember.objects.create(
|
||||||
|
project_id=serializer.data["id"],
|
||||||
|
member_id=serializer.data["project_lead"],
|
||||||
|
role=20,
|
||||||
|
)
|
||||||
|
# Also create the issue property for the user
|
||||||
|
IssueProperty.objects.create(
|
||||||
|
project_id=serializer.data["id"],
|
||||||
|
user_id=serializer.data["project_lead"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Default states
|
||||||
|
states = [
|
||||||
|
{
|
||||||
|
"name": "Backlog",
|
||||||
|
"color": "#A3A3A3",
|
||||||
|
"sequence": 15000,
|
||||||
|
"group": "backlog",
|
||||||
|
"default": True,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Todo",
|
||||||
|
"color": "#3A3A3A",
|
||||||
|
"sequence": 25000,
|
||||||
|
"group": "unstarted",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "In Progress",
|
||||||
|
"color": "#F59E0B",
|
||||||
|
"sequence": 35000,
|
||||||
|
"group": "started",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Done",
|
||||||
|
"color": "#16A34A",
|
||||||
|
"sequence": 45000,
|
||||||
|
"group": "completed",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Cancelled",
|
||||||
|
"color": "#EF4444",
|
||||||
|
"sequence": 55000,
|
||||||
|
"group": "cancelled",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
State.objects.bulk_create(
|
||||||
|
[
|
||||||
|
State(
|
||||||
|
name=state["name"],
|
||||||
|
color=state["color"],
|
||||||
|
project=serializer.instance,
|
||||||
|
sequence=state["sequence"],
|
||||||
|
workspace=serializer.instance.workspace,
|
||||||
|
group=state["group"],
|
||||||
|
default=state.get("default", False),
|
||||||
|
created_by=request.user,
|
||||||
|
)
|
||||||
|
for state in states
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
project = (
|
||||||
|
self.get_queryset()
|
||||||
|
.filter(pk=serializer.data["id"])
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
serializer = ProjectListSerializer(project)
|
||||||
|
return Response(
|
||||||
|
serializer.data, status=status.HTTP_201_CREATED
|
||||||
|
)
|
||||||
|
return Response(
|
||||||
|
serializer.errors,
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
except IntegrityError as e:
|
||||||
|
if "already exists" in str(e):
|
||||||
|
return Response(
|
||||||
|
{"name": "The project name is already taken"},
|
||||||
|
status=status.HTTP_410_GONE,
|
||||||
|
)
|
||||||
|
except Workspace.DoesNotExist as e:
|
||||||
|
return Response(
|
||||||
|
{"error": "Workspace does not exist"},
|
||||||
|
status=status.HTTP_404_NOT_FOUND,
|
||||||
|
)
|
||||||
|
except serializers.ValidationError as e:
|
||||||
|
return Response(
|
||||||
|
{"identifier": "The project identifier is already taken"},
|
||||||
|
status=status.HTTP_410_GONE,
|
||||||
|
)
|
||||||
|
|
||||||
|
def partial_update(self, request, slug, pk=None):
|
||||||
|
try:
|
||||||
|
workspace = Workspace.objects.get(slug=slug)
|
||||||
|
|
||||||
|
project = Project.objects.get(pk=pk)
|
||||||
|
|
||||||
|
serializer = ProjectSerializer(
|
||||||
|
project,
|
||||||
|
data={**request.data},
|
||||||
|
context={"workspace_id": workspace.id},
|
||||||
|
partial=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
if serializer.is_valid():
|
||||||
|
serializer.save()
|
||||||
|
if serializer.data["inbox_view"]:
|
||||||
|
Inbox.objects.get_or_create(
|
||||||
|
name=f"{project.name} Inbox",
|
||||||
|
project=project,
|
||||||
|
is_default=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create the triage state in Backlog group
|
||||||
|
State.objects.get_or_create(
|
||||||
|
name="Triage",
|
||||||
|
group="backlog",
|
||||||
|
description="Default state for managing all Inbox Issues",
|
||||||
|
project_id=pk,
|
||||||
|
color="#ff7700",
|
||||||
|
)
|
||||||
|
|
||||||
|
project = (
|
||||||
|
self.get_queryset()
|
||||||
|
.filter(pk=serializer.data["id"])
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
serializer = ProjectListSerializer(project)
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
return Response(
|
||||||
|
serializer.errors, status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
|
|
||||||
|
except IntegrityError as e:
|
||||||
|
if "already exists" in str(e):
|
||||||
|
return Response(
|
||||||
|
{"name": "The project name is already taken"},
|
||||||
|
status=status.HTTP_410_GONE,
|
||||||
|
)
|
||||||
|
except (Project.DoesNotExist, Workspace.DoesNotExist):
|
||||||
|
return Response(
|
||||||
|
{"error": "Project does not exist"},
|
||||||
|
status=status.HTTP_404_NOT_FOUND,
|
||||||
|
)
|
||||||
|
except serializers.ValidationError as e:
|
||||||
|
return Response(
|
||||||
|
{"identifier": "The project identifier is already taken"},
|
||||||
|
status=status.HTTP_410_GONE,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectIdentifierEndpoint(BaseAPIView):
|
||||||
|
permission_classes = [
|
||||||
|
ProjectBasePermission,
|
||||||
|
]
|
||||||
|
|
||||||
|
def get(self, request, slug):
|
||||||
|
name = request.GET.get("name", "").strip().upper()
|
||||||
|
|
||||||
|
if name == "":
|
||||||
|
return Response(
|
||||||
|
{"error": "Name is required"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
exists = ProjectIdentifier.objects.filter(
|
||||||
|
name=name, workspace__slug=slug
|
||||||
|
).values("id", "name", "project")
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
{"exists": len(exists), "identifiers": exists},
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
|
||||||
|
def delete(self, request, slug):
|
||||||
|
name = request.data.get("name", "").strip().upper()
|
||||||
|
|
||||||
|
if name == "":
|
||||||
|
return Response(
|
||||||
|
{"error": "Name is required"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
if Project.objects.filter(
|
||||||
|
identifier=name, workspace__slug=slug
|
||||||
|
).exists():
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"error": "Cannot delete an identifier of an existing project"
|
||||||
|
},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
ProjectIdentifier.objects.filter(
|
||||||
|
name=name, workspace__slug=slug
|
||||||
|
).delete()
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
status=status.HTTP_204_NO_CONTENT,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectUserViewsEndpoint(BaseAPIView):
|
||||||
|
def post(self, request, slug, project_id):
|
||||||
|
project = Project.objects.get(pk=project_id, workspace__slug=slug)
|
||||||
|
|
||||||
|
project_member = ProjectMember.objects.filter(
|
||||||
|
member=request.user,
|
||||||
|
project=project,
|
||||||
|
is_active=True,
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if project_member is None:
|
||||||
|
return Response(
|
||||||
|
{"error": "Forbidden"}, status=status.HTTP_403_FORBIDDEN
|
||||||
|
)
|
||||||
|
|
||||||
|
view_props = project_member.view_props
|
||||||
|
default_props = project_member.default_props
|
||||||
|
preferences = project_member.preferences
|
||||||
|
sort_order = project_member.sort_order
|
||||||
|
|
||||||
|
project_member.view_props = request.data.get("view_props", view_props)
|
||||||
|
project_member.default_props = request.data.get(
|
||||||
|
"default_props", default_props
|
||||||
|
)
|
||||||
|
project_member.preferences = request.data.get(
|
||||||
|
"preferences", preferences
|
||||||
|
)
|
||||||
|
project_member.sort_order = request.data.get("sort_order", sort_order)
|
||||||
|
|
||||||
|
project_member.save()
|
||||||
|
|
||||||
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectFavoritesViewSet(BaseViewSet):
|
||||||
|
serializer_class = ProjectFavoriteSerializer
|
||||||
|
model = ProjectFavorite
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return self.filter_queryset(
|
||||||
|
super()
|
||||||
|
.get_queryset()
|
||||||
|
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||||
|
.filter(user=self.request.user)
|
||||||
|
.select_related(
|
||||||
|
"project", "project__project_lead", "project__default_assignee"
|
||||||
|
)
|
||||||
|
.select_related("workspace", "workspace__owner")
|
||||||
|
)
|
||||||
|
|
||||||
|
def perform_create(self, serializer):
|
||||||
|
serializer.save(user=self.request.user)
|
||||||
|
|
||||||
|
def create(self, request, slug):
|
||||||
|
serializer = ProjectFavoriteSerializer(data=request.data)
|
||||||
|
if serializer.is_valid():
|
||||||
|
serializer.save(user=request.user)
|
||||||
|
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):
|
||||||
|
project_favorite = ProjectFavorite.objects.get(
|
||||||
|
project=project_id, user=request.user, workspace__slug=slug
|
||||||
|
)
|
||||||
|
project_favorite.delete()
|
||||||
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectPublicCoverImagesEndpoint(BaseAPIView):
|
||||||
|
permission_classes = [
|
||||||
|
AllowAny,
|
||||||
|
]
|
||||||
|
# Cache the below api for 24 hours
|
||||||
|
@cache_response(60 * 60 * 24, user=False)
|
||||||
|
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_STORAGE_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_STORAGE_BUCKET_NAME}.s3.{settings.AWS_REGION}.amazonaws.com/{content['Key']}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response(files, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectDeployBoardViewSet(BaseViewSet):
|
||||||
|
permission_classes = [
|
||||||
|
ProjectMemberPermission,
|
||||||
|
]
|
||||||
|
serializer_class = ProjectDeployBoardSerializer
|
||||||
|
model = ProjectDeployBoard
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return (
|
||||||
|
super()
|
||||||
|
.get_queryset()
|
||||||
|
.filter(
|
||||||
|
workspace__slug=self.kwargs.get("slug"),
|
||||||
|
project_id=self.kwargs.get("project_id"),
|
||||||
|
)
|
||||||
|
.select_related("project")
|
||||||
|
)
|
||||||
|
|
||||||
|
def create(self, request, slug, project_id):
|
||||||
|
comments = request.data.get("comments", False)
|
||||||
|
reactions = request.data.get("reactions", False)
|
||||||
|
inbox = request.data.get("inbox", None)
|
||||||
|
votes = request.data.get("votes", False)
|
||||||
|
views = request.data.get(
|
||||||
|
"views",
|
||||||
|
{
|
||||||
|
"list": True,
|
||||||
|
"kanban": True,
|
||||||
|
"calendar": True,
|
||||||
|
"gantt": True,
|
||||||
|
"spreadsheet": True,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
project_deploy_board, _ = ProjectDeployBoard.objects.get_or_create(
|
||||||
|
anchor=f"{slug}/{project_id}",
|
||||||
|
project_id=project_id,
|
||||||
|
)
|
||||||
|
project_deploy_board.comments = comments
|
||||||
|
project_deploy_board.reactions = reactions
|
||||||
|
project_deploy_board.inbox = inbox
|
||||||
|
project_deploy_board.votes = votes
|
||||||
|
project_deploy_board.views = views
|
||||||
|
|
||||||
|
project_deploy_board.save()
|
||||||
|
|
||||||
|
serializer = ProjectDeployBoardSerializer(project_deploy_board)
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
286
apiserver/plane/app/views/project/invite.py
Normal file
286
apiserver/plane/app/views/project/invite.py
Normal file
@ -0,0 +1,286 @@
|
|||||||
|
# Python imports
|
||||||
|
import jwt
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# Django imports
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.core.validators import validate_email
|
||||||
|
from django.conf import settings
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
# 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 BaseViewSet, BaseAPIView
|
||||||
|
from plane.app.serializers import ProjectMemberInviteSerializer
|
||||||
|
|
||||||
|
from plane.app.permissions import ProjectBasePermission
|
||||||
|
|
||||||
|
from plane.db.models import (
|
||||||
|
ProjectMember,
|
||||||
|
Workspace,
|
||||||
|
ProjectMemberInvite,
|
||||||
|
User,
|
||||||
|
WorkspaceMember,
|
||||||
|
IssueProperty,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectInvitationsViewset(BaseViewSet):
|
||||||
|
serializer_class = ProjectMemberInviteSerializer
|
||||||
|
model = ProjectMemberInvite
|
||||||
|
|
||||||
|
search_fields = []
|
||||||
|
|
||||||
|
permission_classes = [
|
||||||
|
ProjectBasePermission,
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return self.filter_queryset(
|
||||||
|
super()
|
||||||
|
.get_queryset()
|
||||||
|
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||||
|
.filter(project_id=self.kwargs.get("project_id"))
|
||||||
|
.select_related("project")
|
||||||
|
.select_related("workspace", "workspace__owner")
|
||||||
|
)
|
||||||
|
|
||||||
|
def create(self, request, slug, project_id):
|
||||||
|
emails = request.data.get("emails", [])
|
||||||
|
|
||||||
|
# Check if email is provided
|
||||||
|
if not emails:
|
||||||
|
return Response(
|
||||||
|
{"error": "Emails are required"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
requesting_user = ProjectMember.objects.get(
|
||||||
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
member_id=request.user.id,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if any invited user has an higher role
|
||||||
|
if len(
|
||||||
|
[
|
||||||
|
email
|
||||||
|
for email in emails
|
||||||
|
if int(email.get("role", 10)) > requesting_user.role
|
||||||
|
]
|
||||||
|
):
|
||||||
|
return Response(
|
||||||
|
{"error": "You cannot invite a user with higher role"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
workspace = Workspace.objects.get(slug=slug)
|
||||||
|
|
||||||
|
project_invitations = []
|
||||||
|
for email in emails:
|
||||||
|
try:
|
||||||
|
validate_email(email.get("email"))
|
||||||
|
project_invitations.append(
|
||||||
|
ProjectMemberInvite(
|
||||||
|
email=email.get("email").strip().lower(),
|
||||||
|
project_id=project_id,
|
||||||
|
workspace_id=workspace.id,
|
||||||
|
token=jwt.encode(
|
||||||
|
{
|
||||||
|
"email": email,
|
||||||
|
"timestamp": datetime.now().timestamp(),
|
||||||
|
},
|
||||||
|
settings.SECRET_KEY,
|
||||||
|
algorithm="HS256",
|
||||||
|
),
|
||||||
|
role=email.get("role", 10),
|
||||||
|
created_by=request.user,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except ValidationError:
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"error": f"Invalid email - {email} provided a valid email address is required to send the invite"
|
||||||
|
},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create workspace member invite
|
||||||
|
project_invitations = ProjectMemberInvite.objects.bulk_create(
|
||||||
|
project_invitations, batch_size=10, ignore_conflicts=True
|
||||||
|
)
|
||||||
|
current_site = request.META.get("HTTP_ORIGIN")
|
||||||
|
|
||||||
|
# Send invitations
|
||||||
|
for invitation in project_invitations:
|
||||||
|
project_invitations.delay(
|
||||||
|
invitation.email,
|
||||||
|
project_id,
|
||||||
|
invitation.token,
|
||||||
|
current_site,
|
||||||
|
request.user.email,
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"message": "Email sent successfully",
|
||||||
|
},
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class UserProjectInvitationsViewset(BaseViewSet):
|
||||||
|
serializer_class = ProjectMemberInviteSerializer
|
||||||
|
model = ProjectMemberInvite
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return self.filter_queryset(
|
||||||
|
super()
|
||||||
|
.get_queryset()
|
||||||
|
.filter(email=self.request.user.email)
|
||||||
|
.select_related("workspace", "workspace__owner", "project")
|
||||||
|
)
|
||||||
|
|
||||||
|
def create(self, request, slug):
|
||||||
|
project_ids = request.data.get("project_ids", [])
|
||||||
|
|
||||||
|
# Get the workspace user role
|
||||||
|
workspace_member = WorkspaceMember.objects.get(
|
||||||
|
member=request.user,
|
||||||
|
workspace__slug=slug,
|
||||||
|
is_active=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
workspace_role = workspace_member.role
|
||||||
|
workspace = workspace_member.workspace
|
||||||
|
|
||||||
|
# If the user was already part of workspace
|
||||||
|
_ = ProjectMember.objects.filter(
|
||||||
|
workspace__slug=slug,
|
||||||
|
project_id__in=project_ids,
|
||||||
|
member=request.user,
|
||||||
|
).update(is_active=True)
|
||||||
|
|
||||||
|
ProjectMember.objects.bulk_create(
|
||||||
|
[
|
||||||
|
ProjectMember(
|
||||||
|
project_id=project_id,
|
||||||
|
member=request.user,
|
||||||
|
role=15 if workspace_role >= 15 else 10,
|
||||||
|
workspace=workspace,
|
||||||
|
created_by=request.user,
|
||||||
|
)
|
||||||
|
for project_id in project_ids
|
||||||
|
],
|
||||||
|
ignore_conflicts=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
IssueProperty.objects.bulk_create(
|
||||||
|
[
|
||||||
|
IssueProperty(
|
||||||
|
project_id=project_id,
|
||||||
|
user=request.user,
|
||||||
|
workspace=workspace,
|
||||||
|
created_by=request.user,
|
||||||
|
)
|
||||||
|
for project_id in project_ids
|
||||||
|
],
|
||||||
|
ignore_conflicts=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
{"message": "Projects joined successfully"},
|
||||||
|
status=status.HTTP_201_CREATED,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectJoinEndpoint(BaseAPIView):
|
||||||
|
permission_classes = [
|
||||||
|
AllowAny,
|
||||||
|
]
|
||||||
|
|
||||||
|
def post(self, request, slug, project_id, pk):
|
||||||
|
project_invite = ProjectMemberInvite.objects.get(
|
||||||
|
pk=pk,
|
||||||
|
project_id=project_id,
|
||||||
|
workspace__slug=slug,
|
||||||
|
)
|
||||||
|
|
||||||
|
email = request.data.get("email", "")
|
||||||
|
|
||||||
|
if email == "" or project_invite.email != email:
|
||||||
|
return Response(
|
||||||
|
{"error": "You do not have permission to join the project"},
|
||||||
|
status=status.HTTP_403_FORBIDDEN,
|
||||||
|
)
|
||||||
|
|
||||||
|
if project_invite.responded_at is None:
|
||||||
|
project_invite.accepted = request.data.get("accepted", False)
|
||||||
|
project_invite.responded_at = timezone.now()
|
||||||
|
project_invite.save()
|
||||||
|
|
||||||
|
if project_invite.accepted:
|
||||||
|
# Check if the user account exists
|
||||||
|
user = User.objects.filter(email=email).first()
|
||||||
|
|
||||||
|
# Check if user is a part of workspace
|
||||||
|
workspace_member = WorkspaceMember.objects.filter(
|
||||||
|
workspace__slug=slug, member=user
|
||||||
|
).first()
|
||||||
|
# Add him to workspace
|
||||||
|
if workspace_member is None:
|
||||||
|
_ = WorkspaceMember.objects.create(
|
||||||
|
workspace_id=project_invite.workspace_id,
|
||||||
|
member=user,
|
||||||
|
role=(
|
||||||
|
15
|
||||||
|
if project_invite.role >= 15
|
||||||
|
else project_invite.role
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Else make him active
|
||||||
|
workspace_member.is_active = True
|
||||||
|
workspace_member.save()
|
||||||
|
|
||||||
|
# Check if the user was already a member of project then activate the user
|
||||||
|
project_member = ProjectMember.objects.filter(
|
||||||
|
workspace_id=project_invite.workspace_id, member=user
|
||||||
|
).first()
|
||||||
|
if project_member is None:
|
||||||
|
# Create a Project Member
|
||||||
|
_ = ProjectMember.objects.create(
|
||||||
|
workspace_id=project_invite.workspace_id,
|
||||||
|
member=user,
|
||||||
|
role=project_invite.role,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
project_member.is_active = True
|
||||||
|
project_member.role = project_member.role
|
||||||
|
project_member.save()
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
{"message": "Project Invitation Accepted"},
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
{"message": "Project Invitation was not accepted"},
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
{"error": "You have already responded to the invitation request"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get(self, request, slug, project_id, pk):
|
||||||
|
project_invitation = ProjectMemberInvite.objects.get(
|
||||||
|
workspace__slug=slug, project_id=project_id, pk=pk
|
||||||
|
)
|
||||||
|
serializer = ProjectMemberInviteSerializer(project_invitation)
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
349
apiserver/plane/app/views/project/member.py
Normal file
349
apiserver/plane/app/views/project/member.py
Normal file
@ -0,0 +1,349 @@
|
|||||||
|
# Third Party imports
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework import status
|
||||||
|
|
||||||
|
# Module imports
|
||||||
|
from .base import BaseViewSet, BaseAPIView
|
||||||
|
from plane.app.serializers import (
|
||||||
|
ProjectMemberSerializer,
|
||||||
|
ProjectMemberAdminSerializer,
|
||||||
|
ProjectMemberRoleSerializer,
|
||||||
|
)
|
||||||
|
|
||||||
|
from plane.app.permissions import (
|
||||||
|
ProjectBasePermission,
|
||||||
|
ProjectMemberPermission,
|
||||||
|
ProjectLitePermission,
|
||||||
|
WorkspaceUserPermission,
|
||||||
|
)
|
||||||
|
|
||||||
|
from plane.db.models import (
|
||||||
|
Project,
|
||||||
|
ProjectMember,
|
||||||
|
Workspace,
|
||||||
|
TeamMember,
|
||||||
|
IssueProperty,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectMemberViewSet(BaseViewSet):
|
||||||
|
serializer_class = ProjectMemberAdminSerializer
|
||||||
|
model = ProjectMember
|
||||||
|
permission_classes = [
|
||||||
|
ProjectMemberPermission,
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_permissions(self):
|
||||||
|
if self.action == "leave":
|
||||||
|
self.permission_classes = [
|
||||||
|
ProjectLitePermission,
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
self.permission_classes = [
|
||||||
|
ProjectMemberPermission,
|
||||||
|
]
|
||||||
|
|
||||||
|
return super(ProjectMemberViewSet, self).get_permissions()
|
||||||
|
|
||||||
|
search_fields = [
|
||||||
|
"member__display_name",
|
||||||
|
"member__first_name",
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return self.filter_queryset(
|
||||||
|
super()
|
||||||
|
.get_queryset()
|
||||||
|
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||||
|
.filter(project_id=self.kwargs.get("project_id"))
|
||||||
|
.filter(member__is_bot=False)
|
||||||
|
.filter()
|
||||||
|
.select_related("project")
|
||||||
|
.select_related("member")
|
||||||
|
.select_related("workspace", "workspace__owner")
|
||||||
|
)
|
||||||
|
|
||||||
|
def create(self, request, slug, project_id):
|
||||||
|
members = request.data.get("members", [])
|
||||||
|
|
||||||
|
# get the project
|
||||||
|
project = Project.objects.get(pk=project_id, workspace__slug=slug)
|
||||||
|
|
||||||
|
if not len(members):
|
||||||
|
return Response(
|
||||||
|
{"error": "Atleast one member is required"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
bulk_project_members = []
|
||||||
|
bulk_issue_props = []
|
||||||
|
|
||||||
|
project_members = (
|
||||||
|
ProjectMember.objects.filter(
|
||||||
|
workspace__slug=slug,
|
||||||
|
member_id__in=[member.get("member_id") for member in members],
|
||||||
|
)
|
||||||
|
.values("member_id", "sort_order")
|
||||||
|
.order_by("sort_order")
|
||||||
|
)
|
||||||
|
|
||||||
|
bulk_project_members = []
|
||||||
|
member_roles = {
|
||||||
|
member.get("member_id"): member.get("role") for member in members
|
||||||
|
}
|
||||||
|
# Update roles in the members array based on the member_roles dictionary
|
||||||
|
for project_member in ProjectMember.objects.filter(
|
||||||
|
project_id=project_id,
|
||||||
|
member_id__in=[member.get("member_id") for member in members],
|
||||||
|
):
|
||||||
|
project_member.role = member_roles[str(project_member.member_id)]
|
||||||
|
project_member.is_active = True
|
||||||
|
bulk_project_members.append(project_member)
|
||||||
|
|
||||||
|
# Update the roles of the existing members
|
||||||
|
ProjectMember.objects.bulk_update(
|
||||||
|
bulk_project_members, ["is_active", "role"], batch_size=100
|
||||||
|
)
|
||||||
|
|
||||||
|
for member in members:
|
||||||
|
sort_order = [
|
||||||
|
project_member.get("sort_order")
|
||||||
|
for project_member in project_members
|
||||||
|
if str(project_member.get("member_id"))
|
||||||
|
== str(member.get("member_id"))
|
||||||
|
]
|
||||||
|
bulk_project_members.append(
|
||||||
|
ProjectMember(
|
||||||
|
member_id=member.get("member_id"),
|
||||||
|
role=member.get("role", 10),
|
||||||
|
project_id=project_id,
|
||||||
|
workspace_id=project.workspace_id,
|
||||||
|
sort_order=(
|
||||||
|
sort_order[0] - 10000 if len(sort_order) else 65535
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
bulk_issue_props.append(
|
||||||
|
IssueProperty(
|
||||||
|
user_id=member.get("member_id"),
|
||||||
|
project_id=project_id,
|
||||||
|
workspace_id=project.workspace_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
project_members = ProjectMember.objects.bulk_create(
|
||||||
|
bulk_project_members,
|
||||||
|
batch_size=10,
|
||||||
|
ignore_conflicts=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
_ = IssueProperty.objects.bulk_create(
|
||||||
|
bulk_issue_props, batch_size=10, ignore_conflicts=True
|
||||||
|
)
|
||||||
|
|
||||||
|
project_members = ProjectMember.objects.filter(
|
||||||
|
project_id=project_id,
|
||||||
|
member_id__in=[member.get("member_id") for member in members],
|
||||||
|
)
|
||||||
|
serializer = ProjectMemberRoleSerializer(project_members, many=True)
|
||||||
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
|
def list(self, request, slug, project_id):
|
||||||
|
# Get the list of project members for the project
|
||||||
|
project_members = ProjectMember.objects.filter(
|
||||||
|
project_id=project_id,
|
||||||
|
workspace__slug=slug,
|
||||||
|
member__is_bot=False,
|
||||||
|
is_active=True,
|
||||||
|
).select_related("project", "member", "workspace")
|
||||||
|
|
||||||
|
serializer = ProjectMemberRoleSerializer(
|
||||||
|
project_members, fields=("id", "member", "role"), many=True
|
||||||
|
)
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
def partial_update(self, request, slug, project_id, pk):
|
||||||
|
project_member = ProjectMember.objects.get(
|
||||||
|
pk=pk,
|
||||||
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
is_active=True,
|
||||||
|
)
|
||||||
|
if request.user.id == project_member.member_id:
|
||||||
|
return Response(
|
||||||
|
{"error": "You cannot update your own role"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
# Check while updating user roles
|
||||||
|
requested_project_member = ProjectMember.objects.get(
|
||||||
|
project_id=project_id,
|
||||||
|
workspace__slug=slug,
|
||||||
|
member=request.user,
|
||||||
|
is_active=True,
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
"role" in request.data
|
||||||
|
and int(request.data.get("role", project_member.role))
|
||||||
|
> requested_project_member.role
|
||||||
|
):
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"error": "You cannot update a role that is higher than your own role"
|
||||||
|
},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
serializer = ProjectMemberSerializer(
|
||||||
|
project_member, data=request.data, partial=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if serializer.is_valid():
|
||||||
|
serializer.save()
|
||||||
|
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, pk):
|
||||||
|
project_member = ProjectMember.objects.get(
|
||||||
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
pk=pk,
|
||||||
|
member__is_bot=False,
|
||||||
|
is_active=True,
|
||||||
|
)
|
||||||
|
# check requesting user role
|
||||||
|
requesting_project_member = ProjectMember.objects.get(
|
||||||
|
workspace__slug=slug,
|
||||||
|
member=request.user,
|
||||||
|
project_id=project_id,
|
||||||
|
is_active=True,
|
||||||
|
)
|
||||||
|
# User cannot remove himself
|
||||||
|
if str(project_member.id) == str(requesting_project_member.id):
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"error": "You cannot remove yourself from the workspace. Please use leave workspace"
|
||||||
|
},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
# User cannot deactivate higher role
|
||||||
|
if requesting_project_member.role < project_member.role:
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"error": "You cannot remove a user having role higher than you"
|
||||||
|
},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
project_member.is_active = False
|
||||||
|
project_member.save()
|
||||||
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
def leave(self, request, slug, project_id):
|
||||||
|
project_member = ProjectMember.objects.get(
|
||||||
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
member=request.user,
|
||||||
|
is_active=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if the leaving user is the only admin of the project
|
||||||
|
if (
|
||||||
|
project_member.role == 20
|
||||||
|
and not ProjectMember.objects.filter(
|
||||||
|
workspace__slug=slug,
|
||||||
|
project_id=project_id,
|
||||||
|
role=20,
|
||||||
|
is_active=True,
|
||||||
|
).count()
|
||||||
|
> 1
|
||||||
|
):
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"error": "You cannot leave the project as your the only admin of the project you will have to either delete the project or create an another admin",
|
||||||
|
},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
# Deactivate the user
|
||||||
|
project_member.is_active = False
|
||||||
|
project_member.save()
|
||||||
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
|
||||||
|
class AddTeamToProjectEndpoint(BaseAPIView):
|
||||||
|
permission_classes = [
|
||||||
|
ProjectBasePermission,
|
||||||
|
]
|
||||||
|
|
||||||
|
def post(self, request, slug, project_id):
|
||||||
|
team_members = TeamMember.objects.filter(
|
||||||
|
workspace__slug=slug, team__in=request.data.get("teams", [])
|
||||||
|
).values_list("member", flat=True)
|
||||||
|
|
||||||
|
if len(team_members) == 0:
|
||||||
|
return Response(
|
||||||
|
{"error": "No such team exists"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
workspace = Workspace.objects.get(slug=slug)
|
||||||
|
|
||||||
|
project_members = []
|
||||||
|
issue_props = []
|
||||||
|
for member in team_members:
|
||||||
|
project_members.append(
|
||||||
|
ProjectMember(
|
||||||
|
project_id=project_id,
|
||||||
|
member_id=member,
|
||||||
|
workspace=workspace,
|
||||||
|
created_by=request.user,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
issue_props.append(
|
||||||
|
IssueProperty(
|
||||||
|
project_id=project_id,
|
||||||
|
user_id=member,
|
||||||
|
workspace=workspace,
|
||||||
|
created_by=request.user,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
ProjectMember.objects.bulk_create(
|
||||||
|
project_members, batch_size=10, ignore_conflicts=True
|
||||||
|
)
|
||||||
|
|
||||||
|
_ = IssueProperty.objects.bulk_create(
|
||||||
|
issue_props, batch_size=10, ignore_conflicts=True
|
||||||
|
)
|
||||||
|
|
||||||
|
serializer = ProjectMemberSerializer(project_members, many=True)
|
||||||
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectMemberUserEndpoint(BaseAPIView):
|
||||||
|
def get(self, request, slug, project_id):
|
||||||
|
project_member = ProjectMember.objects.get(
|
||||||
|
project_id=project_id,
|
||||||
|
workspace__slug=slug,
|
||||||
|
member=request.user,
|
||||||
|
is_active=True,
|
||||||
|
)
|
||||||
|
serializer = ProjectMemberSerializer(project_member)
|
||||||
|
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
|
class UserProjectRolesEndpoint(BaseAPIView):
|
||||||
|
permission_classes = [
|
||||||
|
WorkspaceUserPermission,
|
||||||
|
]
|
||||||
|
|
||||||
|
def get(self, request, slug):
|
||||||
|
project_members = ProjectMember.objects.filter(
|
||||||
|
workspace__slug=slug,
|
||||||
|
member_id=request.user.id,
|
||||||
|
).values("project_id", "role")
|
||||||
|
|
||||||
|
project_members = {
|
||||||
|
str(member["project_id"]): member["role"]
|
||||||
|
for member in project_members
|
||||||
|
}
|
||||||
|
return Response(project_members, status=status.HTTP_200_OK)
|
@ -9,7 +9,7 @@ from rest_framework.response import Response
|
|||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from . import BaseViewSet
|
from .. import BaseViewSet
|
||||||
from plane.app.serializers import StateSerializer
|
from plane.app.serializers import StateSerializer
|
||||||
from plane.app.permissions import (
|
from plane.app.permissions import (
|
||||||
ProjectEntityPermission,
|
ProjectEntityPermission,
|
@ -23,7 +23,7 @@ from rest_framework.response import Response
|
|||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from . import BaseViewSet
|
from .. import BaseViewSet
|
||||||
from plane.app.serializers import (
|
from plane.app.serializers import (
|
||||||
IssueViewSerializer,
|
IssueViewSerializer,
|
||||||
IssueSerializer,
|
IssueSerializer,
|
@ -8,7 +8,7 @@ from rest_framework.response import Response
|
|||||||
# Module imports
|
# Module imports
|
||||||
from plane.db.models import Webhook, WebhookLog, Workspace
|
from plane.db.models import Webhook, WebhookLog, Workspace
|
||||||
from plane.db.models.webhook import generate_token
|
from plane.db.models.webhook import generate_token
|
||||||
from .base import BaseAPIView
|
from ..base import BaseAPIView
|
||||||
from plane.app.permissions import WorkspaceOwnerPermission
|
from plane.app.permissions import WorkspaceOwnerPermission
|
||||||
from plane.app.serializers import WebhookSerializer, WebhookLogSerializer
|
from plane.app.serializers import WebhookSerializer, WebhookLogSerializer
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
414
apiserver/plane/app/views/workspace/base.py
Normal file
414
apiserver/plane/app/views/workspace/base.py
Normal file
@ -0,0 +1,414 @@
|
|||||||
|
# Python imports
|
||||||
|
from datetime import date
|
||||||
|
from dateutil.relativedelta import relativedelta
|
||||||
|
import csv
|
||||||
|
import io
|
||||||
|
|
||||||
|
|
||||||
|
# Django imports
|
||||||
|
from django.http import HttpResponse
|
||||||
|
from django.db import IntegrityError
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.db.models import (
|
||||||
|
Prefetch,
|
||||||
|
OuterRef,
|
||||||
|
Func,
|
||||||
|
F,
|
||||||
|
Q,
|
||||||
|
Count,
|
||||||
|
)
|
||||||
|
from django.db.models.functions import ExtractWeek, Cast, ExtractDay
|
||||||
|
from django.db.models.fields import DateField
|
||||||
|
|
||||||
|
# Third party modules
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.response import Response
|
||||||
|
|
||||||
|
# Module imports
|
||||||
|
from plane.app.serializers import (
|
||||||
|
WorkSpaceSerializer,
|
||||||
|
WorkspaceThemeSerializer,
|
||||||
|
)
|
||||||
|
from plane.app.views.base import BaseViewSet, BaseAPIView
|
||||||
|
from plane.db.models import (
|
||||||
|
Workspace,
|
||||||
|
IssueActivity,
|
||||||
|
Issue,
|
||||||
|
WorkspaceTheme,
|
||||||
|
WorkspaceMember,
|
||||||
|
)
|
||||||
|
from plane.app.permissions import (
|
||||||
|
WorkSpaceBasePermission,
|
||||||
|
WorkSpaceAdminPermission,
|
||||||
|
WorkspaceEntityPermission,
|
||||||
|
)
|
||||||
|
from plane.utils.cache import cache_response, invalidate_cache
|
||||||
|
|
||||||
|
class WorkSpaceViewSet(BaseViewSet):
|
||||||
|
model = Workspace
|
||||||
|
serializer_class = WorkSpaceSerializer
|
||||||
|
permission_classes = [
|
||||||
|
WorkSpaceBasePermission,
|
||||||
|
]
|
||||||
|
|
||||||
|
search_fields = [
|
||||||
|
"name",
|
||||||
|
]
|
||||||
|
filterset_fields = [
|
||||||
|
"owner",
|
||||||
|
]
|
||||||
|
|
||||||
|
lookup_field = "slug"
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
member_count = (
|
||||||
|
WorkspaceMember.objects.filter(
|
||||||
|
workspace=OuterRef("id"),
|
||||||
|
member__is_bot=False,
|
||||||
|
is_active=True,
|
||||||
|
)
|
||||||
|
.order_by()
|
||||||
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
|
.values("count")
|
||||||
|
)
|
||||||
|
|
||||||
|
issue_count = (
|
||||||
|
Issue.issue_objects.filter(workspace=OuterRef("id"))
|
||||||
|
.order_by()
|
||||||
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
|
.values("count")
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
self.filter_queryset(
|
||||||
|
super().get_queryset().select_related("owner")
|
||||||
|
)
|
||||||
|
.order_by("name")
|
||||||
|
.filter(
|
||||||
|
workspace_member__member=self.request.user,
|
||||||
|
workspace_member__is_active=True,
|
||||||
|
)
|
||||||
|
.annotate(total_members=member_count)
|
||||||
|
.annotate(total_issues=issue_count)
|
||||||
|
.select_related("owner")
|
||||||
|
)
|
||||||
|
|
||||||
|
@invalidate_cache(path="/api/workspaces/", user=False)
|
||||||
|
@invalidate_cache(path="/api/users/me/workspaces/")
|
||||||
|
def create(self, request):
|
||||||
|
try:
|
||||||
|
serializer = WorkSpaceSerializer(data=request.data)
|
||||||
|
|
||||||
|
slug = request.data.get("slug", False)
|
||||||
|
name = request.data.get("name", False)
|
||||||
|
|
||||||
|
if not name or not slug:
|
||||||
|
return Response(
|
||||||
|
{"error": "Both name and slug are required"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(name) > 80 or len(slug) > 48:
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"error": "The maximum length for name is 80 and for slug is 48"
|
||||||
|
},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
if serializer.is_valid():
|
||||||
|
serializer.save(owner=request.user)
|
||||||
|
# Create Workspace member
|
||||||
|
_ = WorkspaceMember.objects.create(
|
||||||
|
workspace_id=serializer.data["id"],
|
||||||
|
member=request.user,
|
||||||
|
role=20,
|
||||||
|
company_role=request.data.get("company_role", ""),
|
||||||
|
)
|
||||||
|
return Response(
|
||||||
|
serializer.data, status=status.HTTP_201_CREATED
|
||||||
|
)
|
||||||
|
return Response(
|
||||||
|
[serializer.errors[error][0] for error in serializer.errors],
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
except IntegrityError as e:
|
||||||
|
if "already exists" in str(e):
|
||||||
|
return Response(
|
||||||
|
{"slug": "The workspace with the slug already exists"},
|
||||||
|
status=status.HTTP_410_GONE,
|
||||||
|
)
|
||||||
|
@cache_response(60 * 60 * 2)
|
||||||
|
def list(self, request, *args, **kwargs):
|
||||||
|
return super().list(request, *args, **kwargs)
|
||||||
|
|
||||||
|
@invalidate_cache(path="/api/workspaces/", user=False)
|
||||||
|
@invalidate_cache(path="/api/users/me/workspaces/")
|
||||||
|
def partial_update(self, request, *args, **kwargs):
|
||||||
|
return super().partial_update(request, *args, **kwargs)
|
||||||
|
|
||||||
|
@invalidate_cache(path="/api/workspaces/", user=False)
|
||||||
|
@invalidate_cache(path="/api/users/me/workspaces/")
|
||||||
|
def destroy(self, request, *args, **kwargs):
|
||||||
|
return super().destroy(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class UserWorkSpacesEndpoint(BaseAPIView):
|
||||||
|
search_fields = [
|
||||||
|
"name",
|
||||||
|
]
|
||||||
|
filterset_fields = [
|
||||||
|
"owner",
|
||||||
|
]
|
||||||
|
|
||||||
|
@cache_response(60 * 60 * 2)
|
||||||
|
def get(self, request):
|
||||||
|
fields = [
|
||||||
|
field
|
||||||
|
for field in request.GET.get("fields", "").split(",")
|
||||||
|
if field
|
||||||
|
]
|
||||||
|
member_count = (
|
||||||
|
WorkspaceMember.objects.filter(
|
||||||
|
workspace=OuterRef("id"),
|
||||||
|
member__is_bot=False,
|
||||||
|
is_active=True,
|
||||||
|
)
|
||||||
|
.order_by()
|
||||||
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
|
.values("count")
|
||||||
|
)
|
||||||
|
|
||||||
|
issue_count = (
|
||||||
|
Issue.issue_objects.filter(workspace=OuterRef("id"))
|
||||||
|
.order_by()
|
||||||
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
|
.values("count")
|
||||||
|
)
|
||||||
|
|
||||||
|
workspace = (
|
||||||
|
Workspace.objects.prefetch_related(
|
||||||
|
Prefetch(
|
||||||
|
"workspace_member",
|
||||||
|
queryset=WorkspaceMember.objects.filter(
|
||||||
|
member=request.user, is_active=True
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.select_related("owner")
|
||||||
|
.annotate(total_members=member_count)
|
||||||
|
.annotate(total_issues=issue_count)
|
||||||
|
.filter(
|
||||||
|
workspace_member__member=request.user,
|
||||||
|
workspace_member__is_active=True,
|
||||||
|
)
|
||||||
|
.distinct()
|
||||||
|
)
|
||||||
|
workspaces = WorkSpaceSerializer(
|
||||||
|
self.filter_queryset(workspace),
|
||||||
|
fields=fields if fields else None,
|
||||||
|
many=True,
|
||||||
|
).data
|
||||||
|
return Response(workspaces, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
|
class WorkSpaceAvailabilityCheckEndpoint(BaseAPIView):
|
||||||
|
def get(self, request):
|
||||||
|
slug = request.GET.get("slug", False)
|
||||||
|
|
||||||
|
if not slug or slug == "":
|
||||||
|
return Response(
|
||||||
|
{"error": "Workspace Slug is required"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
workspace = Workspace.objects.filter(slug=slug).exists()
|
||||||
|
return Response({"status": not workspace}, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
|
class WeekInMonth(Func):
|
||||||
|
function = "FLOOR"
|
||||||
|
template = "(((%(expressions)s - 1) / 7) + 1)::INTEGER"
|
||||||
|
|
||||||
|
|
||||||
|
class UserWorkspaceDashboardEndpoint(BaseAPIView):
|
||||||
|
def get(self, request, slug):
|
||||||
|
issue_activities = (
|
||||||
|
IssueActivity.objects.filter(
|
||||||
|
actor=request.user,
|
||||||
|
workspace__slug=slug,
|
||||||
|
created_at__date__gte=date.today() + relativedelta(months=-3),
|
||||||
|
)
|
||||||
|
.annotate(created_date=Cast("created_at", DateField()))
|
||||||
|
.values("created_date")
|
||||||
|
.annotate(activity_count=Count("created_date"))
|
||||||
|
.order_by("created_date")
|
||||||
|
)
|
||||||
|
|
||||||
|
month = request.GET.get("month", 1)
|
||||||
|
|
||||||
|
completed_issues = (
|
||||||
|
Issue.issue_objects.filter(
|
||||||
|
assignees__in=[request.user],
|
||||||
|
workspace__slug=slug,
|
||||||
|
completed_at__month=month,
|
||||||
|
completed_at__isnull=False,
|
||||||
|
)
|
||||||
|
.annotate(day_of_month=ExtractDay("completed_at"))
|
||||||
|
.annotate(week_in_month=WeekInMonth(F("day_of_month")))
|
||||||
|
.values("week_in_month")
|
||||||
|
.annotate(completed_count=Count("id"))
|
||||||
|
.order_by("week_in_month")
|
||||||
|
)
|
||||||
|
|
||||||
|
assigned_issues = Issue.issue_objects.filter(
|
||||||
|
workspace__slug=slug, assignees__in=[request.user]
|
||||||
|
).count()
|
||||||
|
|
||||||
|
pending_issues_count = Issue.issue_objects.filter(
|
||||||
|
~Q(state__group__in=["completed", "cancelled"]),
|
||||||
|
workspace__slug=slug,
|
||||||
|
assignees__in=[request.user],
|
||||||
|
).count()
|
||||||
|
|
||||||
|
completed_issues_count = Issue.issue_objects.filter(
|
||||||
|
workspace__slug=slug,
|
||||||
|
assignees__in=[request.user],
|
||||||
|
state__group="completed",
|
||||||
|
).count()
|
||||||
|
|
||||||
|
issues_due_week = (
|
||||||
|
Issue.issue_objects.filter(
|
||||||
|
workspace__slug=slug,
|
||||||
|
assignees__in=[request.user],
|
||||||
|
)
|
||||||
|
.annotate(target_week=ExtractWeek("target_date"))
|
||||||
|
.filter(target_week=timezone.now().date().isocalendar()[1])
|
||||||
|
.count()
|
||||||
|
)
|
||||||
|
|
||||||
|
state_distribution = (
|
||||||
|
Issue.issue_objects.filter(
|
||||||
|
workspace__slug=slug, assignees__in=[request.user]
|
||||||
|
)
|
||||||
|
.annotate(state_group=F("state__group"))
|
||||||
|
.values("state_group")
|
||||||
|
.annotate(state_count=Count("state_group"))
|
||||||
|
.order_by("state_group")
|
||||||
|
)
|
||||||
|
|
||||||
|
overdue_issues = Issue.issue_objects.filter(
|
||||||
|
~Q(state__group__in=["completed", "cancelled"]),
|
||||||
|
workspace__slug=slug,
|
||||||
|
assignees__in=[request.user],
|
||||||
|
target_date__lt=timezone.now(),
|
||||||
|
completed_at__isnull=True,
|
||||||
|
).values("id", "name", "workspace__slug", "project_id", "target_date")
|
||||||
|
|
||||||
|
upcoming_issues = Issue.issue_objects.filter(
|
||||||
|
~Q(state__group__in=["completed", "cancelled"]),
|
||||||
|
start_date__gte=timezone.now(),
|
||||||
|
workspace__slug=slug,
|
||||||
|
assignees__in=[request.user],
|
||||||
|
completed_at__isnull=True,
|
||||||
|
).values("id", "name", "workspace__slug", "project_id", "start_date")
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"issue_activities": issue_activities,
|
||||||
|
"completed_issues": completed_issues,
|
||||||
|
"assigned_issues_count": assigned_issues,
|
||||||
|
"pending_issues_count": pending_issues_count,
|
||||||
|
"completed_issues_count": completed_issues_count,
|
||||||
|
"issues_due_week_count": issues_due_week,
|
||||||
|
"state_distribution": state_distribution,
|
||||||
|
"overdue_issues": overdue_issues,
|
||||||
|
"upcoming_issues": upcoming_issues,
|
||||||
|
},
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class WorkspaceThemeViewSet(BaseViewSet):
|
||||||
|
permission_classes = [
|
||||||
|
WorkSpaceAdminPermission,
|
||||||
|
]
|
||||||
|
model = WorkspaceTheme
|
||||||
|
serializer_class = WorkspaceThemeSerializer
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return (
|
||||||
|
super()
|
||||||
|
.get_queryset()
|
||||||
|
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||||
|
)
|
||||||
|
|
||||||
|
def create(self, request, slug):
|
||||||
|
workspace = Workspace.objects.get(slug=slug)
|
||||||
|
serializer = WorkspaceThemeSerializer(data=request.data)
|
||||||
|
if serializer.is_valid():
|
||||||
|
serializer.save(workspace=workspace, actor=request.user)
|
||||||
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
|
||||||
|
class ExportWorkspaceUserActivityEndpoint(BaseAPIView):
|
||||||
|
permission_classes = [
|
||||||
|
WorkspaceEntityPermission,
|
||||||
|
]
|
||||||
|
|
||||||
|
def generate_csv_from_rows(self, rows):
|
||||||
|
"""Generate CSV buffer from rows."""
|
||||||
|
csv_buffer = io.StringIO()
|
||||||
|
writer = csv.writer(csv_buffer, delimiter=",", quoting=csv.QUOTE_ALL)
|
||||||
|
[writer.writerow(row) for row in rows]
|
||||||
|
csv_buffer.seek(0)
|
||||||
|
return csv_buffer
|
||||||
|
|
||||||
|
def post(self, request, slug, user_id):
|
||||||
|
|
||||||
|
if not request.data.get("date"):
|
||||||
|
return Response(
|
||||||
|
{"error": "Date is required"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
user_activities = IssueActivity.objects.filter(
|
||||||
|
~Q(field__in=["comment", "vote", "reaction", "draft"]),
|
||||||
|
workspace__slug=slug,
|
||||||
|
created_at__date=request.data.get("date"),
|
||||||
|
project__project_projectmember__member=request.user,
|
||||||
|
actor_id=user_id,
|
||||||
|
).select_related("actor", "workspace", "issue", "project")[:10000]
|
||||||
|
|
||||||
|
header = [
|
||||||
|
"Actor name",
|
||||||
|
"Issue ID",
|
||||||
|
"Project",
|
||||||
|
"Created at",
|
||||||
|
"Updated at",
|
||||||
|
"Action",
|
||||||
|
"Field",
|
||||||
|
"Old value",
|
||||||
|
"New value",
|
||||||
|
]
|
||||||
|
rows = [
|
||||||
|
(
|
||||||
|
activity.actor.display_name,
|
||||||
|
f"{activity.project.identifier} - {activity.issue.sequence_id if activity.issue else ''}",
|
||||||
|
activity.project.name,
|
||||||
|
activity.created_at,
|
||||||
|
activity.updated_at,
|
||||||
|
activity.verb,
|
||||||
|
activity.field,
|
||||||
|
activity.old_value,
|
||||||
|
activity.new_value,
|
||||||
|
)
|
||||||
|
for activity in user_activities
|
||||||
|
]
|
||||||
|
csv_buffer = self.generate_csv_from_rows([header] + rows)
|
||||||
|
response = HttpResponse(csv_buffer.getvalue(), content_type="text/csv")
|
||||||
|
response["Content-Disposition"] = (
|
||||||
|
'attachment; filename="workspace-user-activity.csv"'
|
||||||
|
)
|
||||||
|
return response
|
116
apiserver/plane/app/views/workspace/cycle.py
Normal file
116
apiserver/plane/app/views/workspace/cycle.py
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
# Django imports
|
||||||
|
from django.db.models import (
|
||||||
|
Q,
|
||||||
|
Count,
|
||||||
|
Sum,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Third party modules
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.response import Response
|
||||||
|
|
||||||
|
# Module imports
|
||||||
|
from plane.app.views.base import BaseAPIView
|
||||||
|
from plane.db.models import Cycle
|
||||||
|
from plane.app.permissions import WorkspaceViewerPermission
|
||||||
|
from plane.app.serializers.cycle import CycleSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class WorkspaceCyclesEndpoint(BaseAPIView):
|
||||||
|
permission_classes = [
|
||||||
|
WorkspaceViewerPermission,
|
||||||
|
]
|
||||||
|
|
||||||
|
def get(self, request, slug):
|
||||||
|
cycles = (
|
||||||
|
Cycle.objects.filter(workspace__slug=slug)
|
||||||
|
.select_related("project")
|
||||||
|
.select_related("workspace")
|
||||||
|
.select_related("owned_by")
|
||||||
|
.annotate(
|
||||||
|
total_issues=Count(
|
||||||
|
"issue_cycle",
|
||||||
|
filter=Q(
|
||||||
|
issue_cycle__issue__archived_at__isnull=True,
|
||||||
|
issue_cycle__issue__is_draft=False,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
completed_issues=Count(
|
||||||
|
"issue_cycle__issue__state__group",
|
||||||
|
filter=Q(
|
||||||
|
issue_cycle__issue__state__group="completed",
|
||||||
|
issue_cycle__issue__archived_at__isnull=True,
|
||||||
|
issue_cycle__issue__is_draft=False,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
cancelled_issues=Count(
|
||||||
|
"issue_cycle__issue__state__group",
|
||||||
|
filter=Q(
|
||||||
|
issue_cycle__issue__state__group="cancelled",
|
||||||
|
issue_cycle__issue__archived_at__isnull=True,
|
||||||
|
issue_cycle__issue__is_draft=False,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
started_issues=Count(
|
||||||
|
"issue_cycle__issue__state__group",
|
||||||
|
filter=Q(
|
||||||
|
issue_cycle__issue__state__group="started",
|
||||||
|
issue_cycle__issue__archived_at__isnull=True,
|
||||||
|
issue_cycle__issue__is_draft=False,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
unstarted_issues=Count(
|
||||||
|
"issue_cycle__issue__state__group",
|
||||||
|
filter=Q(
|
||||||
|
issue_cycle__issue__state__group="unstarted",
|
||||||
|
issue_cycle__issue__archived_at__isnull=True,
|
||||||
|
issue_cycle__issue__is_draft=False,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
backlog_issues=Count(
|
||||||
|
"issue_cycle__issue__state__group",
|
||||||
|
filter=Q(
|
||||||
|
issue_cycle__issue__state__group="backlog",
|
||||||
|
issue_cycle__issue__archived_at__isnull=True,
|
||||||
|
issue_cycle__issue__is_draft=False,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
total_estimates=Sum("issue_cycle__issue__estimate_point")
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
completed_estimates=Sum(
|
||||||
|
"issue_cycle__issue__estimate_point",
|
||||||
|
filter=Q(
|
||||||
|
issue_cycle__issue__state__group="completed",
|
||||||
|
issue_cycle__issue__archived_at__isnull=True,
|
||||||
|
issue_cycle__issue__is_draft=False,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
started_estimates=Sum(
|
||||||
|
"issue_cycle__issue__estimate_point",
|
||||||
|
filter=Q(
|
||||||
|
issue_cycle__issue__state__group="started",
|
||||||
|
issue_cycle__issue__archived_at__isnull=True,
|
||||||
|
issue_cycle__issue__is_draft=False,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.order_by(self.kwargs.get("order_by", "-created_at"))
|
||||||
|
.distinct()
|
||||||
|
)
|
||||||
|
serializer = CycleSerializer(cycles, many=True).data
|
||||||
|
return Response(serializer, status=status.HTTP_200_OK)
|
39
apiserver/plane/app/views/workspace/estimate.py
Normal file
39
apiserver/plane/app/views/workspace/estimate.py
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
# Third party modules
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.response import Response
|
||||||
|
|
||||||
|
# Module imports
|
||||||
|
from plane.app.serializers import WorkspaceEstimateSerializer
|
||||||
|
from plane.app.views.base import BaseAPIView
|
||||||
|
from plane.db.models import Project, Estimate
|
||||||
|
from plane.app.permissions import WorkspaceEntityPermission
|
||||||
|
|
||||||
|
# Django imports
|
||||||
|
from django.db.models import (
|
||||||
|
Prefetch,
|
||||||
|
)
|
||||||
|
from plane.utils.cache import cache_response
|
||||||
|
|
||||||
|
|
||||||
|
class WorkspaceEstimatesEndpoint(BaseAPIView):
|
||||||
|
permission_classes = [
|
||||||
|
WorkspaceEntityPermission,
|
||||||
|
]
|
||||||
|
|
||||||
|
@cache_response(60 * 60 * 2)
|
||||||
|
def get(self, request, slug):
|
||||||
|
estimate_ids = Project.objects.filter(
|
||||||
|
workspace__slug=slug, estimate__isnull=False
|
||||||
|
).values_list("estimate_id", flat=True)
|
||||||
|
estimates = Estimate.objects.filter(
|
||||||
|
pk__in=estimate_ids
|
||||||
|
).prefetch_related(
|
||||||
|
Prefetch(
|
||||||
|
"points",
|
||||||
|
queryset=Project.objects.select_related(
|
||||||
|
"estimate", "workspace", "project"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
serializer = WorkspaceEstimateSerializer(estimates, many=True)
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
301
apiserver/plane/app/views/workspace/invite.py
Normal file
301
apiserver/plane/app/views/workspace/invite.py
Normal file
@ -0,0 +1,301 @@
|
|||||||
|
# Python imports
|
||||||
|
import jwt
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# Django imports
|
||||||
|
from django.conf import settings
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.db.models import Count
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.core.validators import validate_email
|
||||||
|
|
||||||
|
# Third party modules
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.permissions import AllowAny
|
||||||
|
|
||||||
|
# Module imports
|
||||||
|
from plane.app.serializers import (
|
||||||
|
WorkSpaceMemberSerializer,
|
||||||
|
WorkSpaceMemberInviteSerializer,
|
||||||
|
)
|
||||||
|
from plane.app.views.base import BaseAPIView
|
||||||
|
from .. import BaseViewSet
|
||||||
|
from plane.db.models import (
|
||||||
|
User,
|
||||||
|
Workspace,
|
||||||
|
WorkspaceMemberInvite,
|
||||||
|
WorkspaceMember,
|
||||||
|
)
|
||||||
|
from plane.app.permissions import WorkSpaceAdminPermission
|
||||||
|
from plane.bgtasks.workspace_invitation_task import workspace_invitation
|
||||||
|
from plane.bgtasks.event_tracking_task import workspace_invite_event
|
||||||
|
from plane.utils.cache import invalidate_cache
|
||||||
|
|
||||||
|
class WorkspaceInvitationsViewset(BaseViewSet):
|
||||||
|
"""Endpoint for creating, listing and deleting workspaces"""
|
||||||
|
|
||||||
|
serializer_class = WorkSpaceMemberInviteSerializer
|
||||||
|
model = WorkspaceMemberInvite
|
||||||
|
|
||||||
|
permission_classes = [
|
||||||
|
WorkSpaceAdminPermission,
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return self.filter_queryset(
|
||||||
|
super()
|
||||||
|
.get_queryset()
|
||||||
|
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||||
|
.select_related("workspace", "workspace__owner", "created_by")
|
||||||
|
)
|
||||||
|
|
||||||
|
def create(self, request, slug):
|
||||||
|
emails = request.data.get("emails", [])
|
||||||
|
# Check if email is provided
|
||||||
|
if not emails:
|
||||||
|
return Response(
|
||||||
|
{"error": "Emails are required"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
# check for role level of the requesting user
|
||||||
|
requesting_user = WorkspaceMember.objects.get(
|
||||||
|
workspace__slug=slug,
|
||||||
|
member=request.user,
|
||||||
|
is_active=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if any invited user has an higher role
|
||||||
|
if len(
|
||||||
|
[
|
||||||
|
email
|
||||||
|
for email in emails
|
||||||
|
if int(email.get("role", 10)) > requesting_user.role
|
||||||
|
]
|
||||||
|
):
|
||||||
|
return Response(
|
||||||
|
{"error": "You cannot invite a user with higher role"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get the workspace object
|
||||||
|
workspace = Workspace.objects.get(slug=slug)
|
||||||
|
|
||||||
|
# Check if user is already a member of workspace
|
||||||
|
workspace_members = WorkspaceMember.objects.filter(
|
||||||
|
workspace_id=workspace.id,
|
||||||
|
member__email__in=[email.get("email") for email in emails],
|
||||||
|
is_active=True,
|
||||||
|
).select_related("member", "workspace", "workspace__owner")
|
||||||
|
|
||||||
|
if workspace_members:
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"error": "Some users are already member of workspace",
|
||||||
|
"workspace_users": WorkSpaceMemberSerializer(
|
||||||
|
workspace_members, many=True
|
||||||
|
).data,
|
||||||
|
},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
workspace_invitations = []
|
||||||
|
for email in emails:
|
||||||
|
try:
|
||||||
|
validate_email(email.get("email"))
|
||||||
|
workspace_invitations.append(
|
||||||
|
WorkspaceMemberInvite(
|
||||||
|
email=email.get("email").strip().lower(),
|
||||||
|
workspace_id=workspace.id,
|
||||||
|
token=jwt.encode(
|
||||||
|
{
|
||||||
|
"email": email,
|
||||||
|
"timestamp": datetime.now().timestamp(),
|
||||||
|
},
|
||||||
|
settings.SECRET_KEY,
|
||||||
|
algorithm="HS256",
|
||||||
|
),
|
||||||
|
role=email.get("role", 10),
|
||||||
|
created_by=request.user,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except ValidationError:
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"error": f"Invalid email - {email} provided a valid email address is required to send the invite"
|
||||||
|
},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
# Create workspace member invite
|
||||||
|
workspace_invitations = WorkspaceMemberInvite.objects.bulk_create(
|
||||||
|
workspace_invitations, batch_size=10, ignore_conflicts=True
|
||||||
|
)
|
||||||
|
|
||||||
|
current_site = request.META.get("HTTP_ORIGIN")
|
||||||
|
|
||||||
|
# Send invitations
|
||||||
|
for invitation in workspace_invitations:
|
||||||
|
workspace_invitation.delay(
|
||||||
|
invitation.email,
|
||||||
|
workspace.id,
|
||||||
|
invitation.token,
|
||||||
|
current_site,
|
||||||
|
request.user.email,
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"message": "Emails sent successfully",
|
||||||
|
},
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
|
||||||
|
def destroy(self, request, slug, pk):
|
||||||
|
workspace_member_invite = WorkspaceMemberInvite.objects.get(
|
||||||
|
pk=pk, workspace__slug=slug
|
||||||
|
)
|
||||||
|
workspace_member_invite.delete()
|
||||||
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
|
||||||
|
class WorkspaceJoinEndpoint(BaseAPIView):
|
||||||
|
permission_classes = [
|
||||||
|
AllowAny,
|
||||||
|
]
|
||||||
|
"""Invitation response endpoint the user can respond to the invitation"""
|
||||||
|
|
||||||
|
@invalidate_cache(path="/api/workspaces/", user=False)
|
||||||
|
@invalidate_cache(path="/api/users/me/workspaces/")
|
||||||
|
def post(self, request, slug, pk):
|
||||||
|
workspace_invite = WorkspaceMemberInvite.objects.get(
|
||||||
|
pk=pk, workspace__slug=slug
|
||||||
|
)
|
||||||
|
|
||||||
|
email = request.data.get("email", "")
|
||||||
|
|
||||||
|
# Check the email
|
||||||
|
if email == "" or workspace_invite.email != email:
|
||||||
|
return Response(
|
||||||
|
{"error": "You do not have permission to join the workspace"},
|
||||||
|
status=status.HTTP_403_FORBIDDEN,
|
||||||
|
)
|
||||||
|
|
||||||
|
# If already responded then return error
|
||||||
|
if workspace_invite.responded_at is None:
|
||||||
|
workspace_invite.accepted = request.data.get("accepted", False)
|
||||||
|
workspace_invite.responded_at = timezone.now()
|
||||||
|
workspace_invite.save()
|
||||||
|
|
||||||
|
if workspace_invite.accepted:
|
||||||
|
# Check if the user created account after invitation
|
||||||
|
user = User.objects.filter(email=email).first()
|
||||||
|
|
||||||
|
# If the user is present then create the workspace member
|
||||||
|
if user is not None:
|
||||||
|
# Check if the user was already a member of workspace then activate the user
|
||||||
|
workspace_member = WorkspaceMember.objects.filter(
|
||||||
|
workspace=workspace_invite.workspace, member=user
|
||||||
|
).first()
|
||||||
|
if workspace_member is not None:
|
||||||
|
workspace_member.is_active = True
|
||||||
|
workspace_member.role = workspace_invite.role
|
||||||
|
workspace_member.save()
|
||||||
|
else:
|
||||||
|
# Create a Workspace
|
||||||
|
_ = WorkspaceMember.objects.create(
|
||||||
|
workspace=workspace_invite.workspace,
|
||||||
|
member=user,
|
||||||
|
role=workspace_invite.role,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Set the user last_workspace_id to the accepted workspace
|
||||||
|
user.last_workspace_id = workspace_invite.workspace.id
|
||||||
|
user.save()
|
||||||
|
|
||||||
|
# Delete the invitation
|
||||||
|
workspace_invite.delete()
|
||||||
|
|
||||||
|
# Send event
|
||||||
|
workspace_invite_event.delay(
|
||||||
|
user=user.id if user is not None else None,
|
||||||
|
email=email,
|
||||||
|
user_agent=request.META.get("HTTP_USER_AGENT"),
|
||||||
|
ip=request.META.get("REMOTE_ADDR"),
|
||||||
|
event_name="MEMBER_ACCEPTED",
|
||||||
|
accepted_from="EMAIL",
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
{"message": "Workspace Invitation Accepted"},
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Workspace invitation rejected
|
||||||
|
return Response(
|
||||||
|
{"message": "Workspace Invitation was not accepted"},
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
{"error": "You have already responded to the invitation request"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get(self, request, slug, pk):
|
||||||
|
workspace_invitation = WorkspaceMemberInvite.objects.get(
|
||||||
|
workspace__slug=slug, pk=pk
|
||||||
|
)
|
||||||
|
serializer = WorkSpaceMemberInviteSerializer(workspace_invitation)
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
|
class UserWorkspaceInvitationsViewSet(BaseViewSet):
|
||||||
|
serializer_class = WorkSpaceMemberInviteSerializer
|
||||||
|
model = WorkspaceMemberInvite
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return self.filter_queryset(
|
||||||
|
super()
|
||||||
|
.get_queryset()
|
||||||
|
.filter(email=self.request.user.email)
|
||||||
|
.select_related("workspace", "workspace__owner", "created_by")
|
||||||
|
.annotate(total_members=Count("workspace__workspace_member"))
|
||||||
|
)
|
||||||
|
|
||||||
|
@invalidate_cache(path="/api/workspaces/", user=False)
|
||||||
|
@invalidate_cache(path="/api/users/me/workspaces/")
|
||||||
|
@invalidate_cache(
|
||||||
|
path="/api/workspaces/:slug/members/", url_params=True, user=False
|
||||||
|
)
|
||||||
|
def create(self, request):
|
||||||
|
invitations = request.data.get("invitations", [])
|
||||||
|
workspace_invitations = WorkspaceMemberInvite.objects.filter(
|
||||||
|
pk__in=invitations, email=request.user.email
|
||||||
|
).order_by("-created_at")
|
||||||
|
|
||||||
|
# If the user is already a member of workspace and was deactivated then activate the user
|
||||||
|
for invitation in workspace_invitations:
|
||||||
|
# Update the WorkspaceMember for this specific invitation
|
||||||
|
WorkspaceMember.objects.filter(
|
||||||
|
workspace_id=invitation.workspace_id, member=request.user
|
||||||
|
).update(is_active=True, role=invitation.role)
|
||||||
|
|
||||||
|
# Bulk create the user for all the workspaces
|
||||||
|
WorkspaceMember.objects.bulk_create(
|
||||||
|
[
|
||||||
|
WorkspaceMember(
|
||||||
|
workspace=invitation.workspace,
|
||||||
|
member=request.user,
|
||||||
|
role=invitation.role,
|
||||||
|
created_by=request.user,
|
||||||
|
)
|
||||||
|
for invitation in workspace_invitations
|
||||||
|
],
|
||||||
|
ignore_conflicts=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Delete joined workspace invites
|
||||||
|
workspace_invitations.delete()
|
||||||
|
|
||||||
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
25
apiserver/plane/app/views/workspace/label.py
Normal file
25
apiserver/plane/app/views/workspace/label.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
# Third party modules
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.response import Response
|
||||||
|
|
||||||
|
# Module imports
|
||||||
|
from plane.app.serializers import LabelSerializer
|
||||||
|
from plane.app.views.base import BaseAPIView
|
||||||
|
from plane.db.models import Label
|
||||||
|
from plane.app.permissions import WorkspaceViewerPermission
|
||||||
|
from plane.utils.cache import cache_response
|
||||||
|
|
||||||
|
class WorkspaceLabelsEndpoint(BaseAPIView):
|
||||||
|
permission_classes = [
|
||||||
|
WorkspaceViewerPermission,
|
||||||
|
]
|
||||||
|
|
||||||
|
@cache_response(60 * 60 * 2)
|
||||||
|
def get(self, request, slug):
|
||||||
|
labels = Label.objects.filter(
|
||||||
|
workspace__slug=slug,
|
||||||
|
project__project_projectmember__member=request.user,
|
||||||
|
project__project_projectmember__is_active=True,
|
||||||
|
)
|
||||||
|
serializer = LabelSerializer(labels, many=True).data
|
||||||
|
return Response(serializer, status=status.HTTP_200_OK)
|
396
apiserver/plane/app/views/workspace/member.py
Normal file
396
apiserver/plane/app/views/workspace/member.py
Normal file
@ -0,0 +1,396 @@
|
|||||||
|
# Django imports
|
||||||
|
from django.db.models import (
|
||||||
|
Q,
|
||||||
|
Count,
|
||||||
|
)
|
||||||
|
from django.db.models.functions import Cast
|
||||||
|
from django.db.models import CharField
|
||||||
|
|
||||||
|
# Third party modules
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.response import Response
|
||||||
|
|
||||||
|
# Module imports
|
||||||
|
from plane.app.serializers import (
|
||||||
|
WorkSpaceMemberSerializer,
|
||||||
|
TeamSerializer,
|
||||||
|
UserLiteSerializer,
|
||||||
|
WorkspaceMemberAdminSerializer,
|
||||||
|
WorkspaceMemberMeSerializer,
|
||||||
|
ProjectMemberRoleSerializer,
|
||||||
|
)
|
||||||
|
from plane.app.views.base import BaseAPIView
|
||||||
|
from .. import BaseViewSet
|
||||||
|
from plane.db.models import (
|
||||||
|
User,
|
||||||
|
Workspace,
|
||||||
|
Team,
|
||||||
|
ProjectMember,
|
||||||
|
Project,
|
||||||
|
WorkspaceMember,
|
||||||
|
)
|
||||||
|
from plane.app.permissions import (
|
||||||
|
WorkSpaceAdminPermission,
|
||||||
|
WorkspaceEntityPermission,
|
||||||
|
WorkspaceUserPermission,
|
||||||
|
)
|
||||||
|
from plane.utils.cache import cache_response, invalidate_cache
|
||||||
|
|
||||||
|
|
||||||
|
class WorkSpaceMemberViewSet(BaseViewSet):
|
||||||
|
serializer_class = WorkspaceMemberAdminSerializer
|
||||||
|
model = WorkspaceMember
|
||||||
|
|
||||||
|
permission_classes = [
|
||||||
|
WorkspaceEntityPermission,
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_permissions(self):
|
||||||
|
if self.action == "leave":
|
||||||
|
self.permission_classes = [
|
||||||
|
WorkspaceUserPermission,
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
self.permission_classes = [
|
||||||
|
WorkspaceEntityPermission,
|
||||||
|
]
|
||||||
|
|
||||||
|
return super(WorkSpaceMemberViewSet, self).get_permissions()
|
||||||
|
|
||||||
|
search_fields = [
|
||||||
|
"member__display_name",
|
||||||
|
"member__first_name",
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return self.filter_queryset(
|
||||||
|
super()
|
||||||
|
.get_queryset()
|
||||||
|
.filter(
|
||||||
|
workspace__slug=self.kwargs.get("slug"),
|
||||||
|
is_active=True,
|
||||||
|
)
|
||||||
|
.select_related("workspace", "workspace__owner")
|
||||||
|
.select_related("member")
|
||||||
|
)
|
||||||
|
|
||||||
|
@cache_response(60 * 60 * 2)
|
||||||
|
def list(self, request, slug):
|
||||||
|
workspace_member = WorkspaceMember.objects.get(
|
||||||
|
member=request.user,
|
||||||
|
workspace__slug=slug,
|
||||||
|
is_active=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get all active workspace members
|
||||||
|
workspace_members = self.get_queryset()
|
||||||
|
|
||||||
|
if workspace_member.role > 10:
|
||||||
|
serializer = WorkspaceMemberAdminSerializer(
|
||||||
|
workspace_members,
|
||||||
|
fields=("id", "member", "role"),
|
||||||
|
many=True,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
serializer = WorkSpaceMemberSerializer(
|
||||||
|
workspace_members,
|
||||||
|
fields=("id", "member", "role"),
|
||||||
|
many=True,
|
||||||
|
)
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
@invalidate_cache(
|
||||||
|
path="/api/workspaces/:slug/members/", url_params=True, user=False
|
||||||
|
)
|
||||||
|
def partial_update(self, request, slug, pk):
|
||||||
|
workspace_member = WorkspaceMember.objects.get(
|
||||||
|
pk=pk,
|
||||||
|
workspace__slug=slug,
|
||||||
|
member__is_bot=False,
|
||||||
|
is_active=True,
|
||||||
|
)
|
||||||
|
if request.user.id == workspace_member.member_id:
|
||||||
|
return Response(
|
||||||
|
{"error": "You cannot update your own role"},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get the requested user role
|
||||||
|
requested_workspace_member = WorkspaceMember.objects.get(
|
||||||
|
workspace__slug=slug,
|
||||||
|
member=request.user,
|
||||||
|
is_active=True,
|
||||||
|
)
|
||||||
|
# Check if role is being updated
|
||||||
|
# One cannot update role higher than his own role
|
||||||
|
if (
|
||||||
|
"role" in request.data
|
||||||
|
and int(request.data.get("role", workspace_member.role))
|
||||||
|
> requested_workspace_member.role
|
||||||
|
):
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"error": "You cannot update a role that is higher than your own role"
|
||||||
|
},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
serializer = WorkSpaceMemberSerializer(
|
||||||
|
workspace_member, data=request.data, partial=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if serializer.is_valid():
|
||||||
|
serializer.save()
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
@invalidate_cache(
|
||||||
|
path="/api/workspaces/:slug/members/", url_params=True, user=False
|
||||||
|
)
|
||||||
|
def destroy(self, request, slug, pk):
|
||||||
|
# Check the user role who is deleting the user
|
||||||
|
workspace_member = WorkspaceMember.objects.get(
|
||||||
|
workspace__slug=slug,
|
||||||
|
pk=pk,
|
||||||
|
member__is_bot=False,
|
||||||
|
is_active=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# check requesting user role
|
||||||
|
requesting_workspace_member = WorkspaceMember.objects.get(
|
||||||
|
workspace__slug=slug,
|
||||||
|
member=request.user,
|
||||||
|
is_active=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
if str(workspace_member.id) == str(requesting_workspace_member.id):
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"error": "You cannot remove yourself from the workspace. Please use leave workspace"
|
||||||
|
},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
if requesting_workspace_member.role < workspace_member.role:
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"error": "You cannot remove a user having role higher than you"
|
||||||
|
},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
Project.objects.annotate(
|
||||||
|
total_members=Count("project_projectmember"),
|
||||||
|
member_with_role=Count(
|
||||||
|
"project_projectmember",
|
||||||
|
filter=Q(
|
||||||
|
project_projectmember__member_id=workspace_member.id,
|
||||||
|
project_projectmember__role=20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.filter(total_members=1, member_with_role=1, workspace__slug=slug)
|
||||||
|
.exists()
|
||||||
|
):
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"error": "User is a part of some projects where they are the only admin, they should either leave that project or promote another user to admin."
|
||||||
|
},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Deactivate the users from the projects where the user is part of
|
||||||
|
_ = ProjectMember.objects.filter(
|
||||||
|
workspace__slug=slug,
|
||||||
|
member_id=workspace_member.member_id,
|
||||||
|
is_active=True,
|
||||||
|
).update(is_active=False)
|
||||||
|
|
||||||
|
workspace_member.is_active = False
|
||||||
|
workspace_member.save()
|
||||||
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
@invalidate_cache(
|
||||||
|
path="/api/workspaces/:slug/members/", url_params=True, user=False
|
||||||
|
)
|
||||||
|
def leave(self, request, slug):
|
||||||
|
workspace_member = WorkspaceMember.objects.get(
|
||||||
|
workspace__slug=slug,
|
||||||
|
member=request.user,
|
||||||
|
is_active=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if the leaving user is the only admin of the workspace
|
||||||
|
if (
|
||||||
|
workspace_member.role == 20
|
||||||
|
and not WorkspaceMember.objects.filter(
|
||||||
|
workspace__slug=slug,
|
||||||
|
role=20,
|
||||||
|
is_active=True,
|
||||||
|
).count()
|
||||||
|
> 1
|
||||||
|
):
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"error": "You cannot leave the workspace as you are the only admin of the workspace you will have to either delete the workspace or promote another user to admin."
|
||||||
|
},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
Project.objects.annotate(
|
||||||
|
total_members=Count("project_projectmember"),
|
||||||
|
member_with_role=Count(
|
||||||
|
"project_projectmember",
|
||||||
|
filter=Q(
|
||||||
|
project_projectmember__member_id=request.user.id,
|
||||||
|
project_projectmember__role=20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.filter(total_members=1, member_with_role=1, workspace__slug=slug)
|
||||||
|
.exists()
|
||||||
|
):
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"error": "You are a part of some projects where you are the only admin, you should either leave the project or promote another user to admin."
|
||||||
|
},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
# # Deactivate the users from the projects where the user is part of
|
||||||
|
_ = ProjectMember.objects.filter(
|
||||||
|
workspace__slug=slug,
|
||||||
|
member_id=workspace_member.member_id,
|
||||||
|
is_active=True,
|
||||||
|
).update(is_active=False)
|
||||||
|
|
||||||
|
# # Deactivate the user
|
||||||
|
workspace_member.is_active = False
|
||||||
|
workspace_member.save()
|
||||||
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
|
||||||
|
class WorkspaceMemberUserViewsEndpoint(BaseAPIView):
|
||||||
|
def post(self, request, slug):
|
||||||
|
workspace_member = WorkspaceMember.objects.get(
|
||||||
|
workspace__slug=slug,
|
||||||
|
member=request.user,
|
||||||
|
is_active=True,
|
||||||
|
)
|
||||||
|
workspace_member.view_props = request.data.get("view_props", {})
|
||||||
|
workspace_member.save()
|
||||||
|
|
||||||
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
|
||||||
|
class WorkspaceMemberUserEndpoint(BaseAPIView):
|
||||||
|
def get(self, request, slug):
|
||||||
|
workspace_member = WorkspaceMember.objects.get(
|
||||||
|
member=request.user,
|
||||||
|
workspace__slug=slug,
|
||||||
|
is_active=True,
|
||||||
|
)
|
||||||
|
serializer = WorkspaceMemberMeSerializer(workspace_member)
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
|
class WorkspaceProjectMemberEndpoint(BaseAPIView):
|
||||||
|
serializer_class = ProjectMemberRoleSerializer
|
||||||
|
model = ProjectMember
|
||||||
|
|
||||||
|
permission_classes = [
|
||||||
|
WorkspaceEntityPermission,
|
||||||
|
]
|
||||||
|
|
||||||
|
def get(self, request, slug):
|
||||||
|
# Fetch all project IDs where the user is involved
|
||||||
|
project_ids = (
|
||||||
|
ProjectMember.objects.filter(
|
||||||
|
member=request.user,
|
||||||
|
is_active=True,
|
||||||
|
)
|
||||||
|
.values_list("project_id", flat=True)
|
||||||
|
.distinct()
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get all the project members in which the user is involved
|
||||||
|
project_members = ProjectMember.objects.filter(
|
||||||
|
workspace__slug=slug,
|
||||||
|
project_id__in=project_ids,
|
||||||
|
is_active=True,
|
||||||
|
).select_related("project", "member", "workspace")
|
||||||
|
project_members = ProjectMemberRoleSerializer(
|
||||||
|
project_members, many=True
|
||||||
|
).data
|
||||||
|
|
||||||
|
project_members_dict = dict()
|
||||||
|
|
||||||
|
# Construct a dictionary with project_id as key and project_members as value
|
||||||
|
for project_member in project_members:
|
||||||
|
project_id = project_member.pop("project")
|
||||||
|
if str(project_id) not in project_members_dict:
|
||||||
|
project_members_dict[str(project_id)] = []
|
||||||
|
project_members_dict[str(project_id)].append(project_member)
|
||||||
|
|
||||||
|
return Response(project_members_dict, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
|
class TeamMemberViewSet(BaseViewSet):
|
||||||
|
serializer_class = TeamSerializer
|
||||||
|
model = Team
|
||||||
|
permission_classes = [
|
||||||
|
WorkSpaceAdminPermission,
|
||||||
|
]
|
||||||
|
|
||||||
|
search_fields = [
|
||||||
|
"member__display_name",
|
||||||
|
"member__first_name",
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return self.filter_queryset(
|
||||||
|
super()
|
||||||
|
.get_queryset()
|
||||||
|
.filter(workspace__slug=self.kwargs.get("slug"))
|
||||||
|
.select_related("workspace", "workspace__owner")
|
||||||
|
.prefetch_related("members")
|
||||||
|
)
|
||||||
|
|
||||||
|
def create(self, request, slug):
|
||||||
|
members = list(
|
||||||
|
WorkspaceMember.objects.filter(
|
||||||
|
workspace__slug=slug,
|
||||||
|
member__id__in=request.data.get("members", []),
|
||||||
|
is_active=True,
|
||||||
|
)
|
||||||
|
.annotate(member_str_id=Cast("member", output_field=CharField()))
|
||||||
|
.distinct()
|
||||||
|
.values_list("member_str_id", flat=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(members) != len(request.data.get("members", [])):
|
||||||
|
users = list(
|
||||||
|
set(request.data.get("members", [])).difference(members)
|
||||||
|
)
|
||||||
|
users = User.objects.filter(pk__in=users)
|
||||||
|
|
||||||
|
serializer = UserLiteSerializer(users, many=True)
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"error": f"{len(users)} of the member(s) are not a part of the workspace",
|
||||||
|
"members": serializer.data,
|
||||||
|
},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
)
|
||||||
|
|
||||||
|
workspace = Workspace.objects.get(slug=slug)
|
||||||
|
|
||||||
|
serializer = TeamSerializer(
|
||||||
|
data=request.data, context={"workspace": workspace}
|
||||||
|
)
|
||||||
|
if serializer.is_valid():
|
||||||
|
serializer.save()
|
||||||
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
104
apiserver/plane/app/views/workspace/module.py
Normal file
104
apiserver/plane/app/views/workspace/module.py
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
# Django imports
|
||||||
|
from django.db.models import (
|
||||||
|
Prefetch,
|
||||||
|
Q,
|
||||||
|
Count,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Third party modules
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.response import Response
|
||||||
|
|
||||||
|
# Module imports
|
||||||
|
from plane.app.views.base import BaseAPIView
|
||||||
|
from plane.db.models import (
|
||||||
|
Module,
|
||||||
|
ModuleLink,
|
||||||
|
)
|
||||||
|
from plane.app.permissions import WorkspaceViewerPermission
|
||||||
|
from plane.app.serializers.module import ModuleSerializer
|
||||||
|
|
||||||
|
class WorkspaceModulesEndpoint(BaseAPIView):
|
||||||
|
permission_classes = [
|
||||||
|
WorkspaceViewerPermission,
|
||||||
|
]
|
||||||
|
|
||||||
|
def get(self, request, slug):
|
||||||
|
modules = (
|
||||||
|
Module.objects.filter(workspace__slug=slug)
|
||||||
|
.select_related("project")
|
||||||
|
.select_related("workspace")
|
||||||
|
.select_related("lead")
|
||||||
|
.prefetch_related("members")
|
||||||
|
.prefetch_related(
|
||||||
|
Prefetch(
|
||||||
|
"link_module",
|
||||||
|
queryset=ModuleLink.objects.select_related(
|
||||||
|
"module", "created_by"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
total_issues=Count(
|
||||||
|
"issue_module",
|
||||||
|
filter=Q(
|
||||||
|
issue_module__issue__archived_at__isnull=True,
|
||||||
|
issue_module__issue__is_draft=False,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
completed_issues=Count(
|
||||||
|
"issue_module__issue__state__group",
|
||||||
|
filter=Q(
|
||||||
|
issue_module__issue__state__group="completed",
|
||||||
|
issue_module__issue__archived_at__isnull=True,
|
||||||
|
issue_module__issue__is_draft=False,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
cancelled_issues=Count(
|
||||||
|
"issue_module__issue__state__group",
|
||||||
|
filter=Q(
|
||||||
|
issue_module__issue__state__group="cancelled",
|
||||||
|
issue_module__issue__archived_at__isnull=True,
|
||||||
|
issue_module__issue__is_draft=False,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
started_issues=Count(
|
||||||
|
"issue_module__issue__state__group",
|
||||||
|
filter=Q(
|
||||||
|
issue_module__issue__state__group="started",
|
||||||
|
issue_module__issue__archived_at__isnull=True,
|
||||||
|
issue_module__issue__is_draft=False,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
unstarted_issues=Count(
|
||||||
|
"issue_module__issue__state__group",
|
||||||
|
filter=Q(
|
||||||
|
issue_module__issue__state__group="unstarted",
|
||||||
|
issue_module__issue__archived_at__isnull=True,
|
||||||
|
issue_module__issue__is_draft=False,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
backlog_issues=Count(
|
||||||
|
"issue_module__issue__state__group",
|
||||||
|
filter=Q(
|
||||||
|
issue_module__issue__state__group="backlog",
|
||||||
|
issue_module__issue__archived_at__isnull=True,
|
||||||
|
issue_module__issue__is_draft=False,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.order_by(self.kwargs.get("order_by", "-created_at"))
|
||||||
|
)
|
||||||
|
|
||||||
|
serializer = ModuleSerializer(modules, many=True).data
|
||||||
|
return Response(serializer, status=status.HTTP_200_OK)
|
25
apiserver/plane/app/views/workspace/state.py
Normal file
25
apiserver/plane/app/views/workspace/state.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
# Third party modules
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.response import Response
|
||||||
|
|
||||||
|
# Module imports
|
||||||
|
from plane.app.serializers import StateSerializer
|
||||||
|
from plane.app.views.base import BaseAPIView
|
||||||
|
from plane.db.models import State
|
||||||
|
from plane.app.permissions import WorkspaceEntityPermission
|
||||||
|
from plane.utils.cache import cache_response
|
||||||
|
|
||||||
|
class WorkspaceStatesEndpoint(BaseAPIView):
|
||||||
|
permission_classes = [
|
||||||
|
WorkspaceEntityPermission,
|
||||||
|
]
|
||||||
|
|
||||||
|
@cache_response(60 * 60 * 2)
|
||||||
|
def get(self, request, slug):
|
||||||
|
states = State.objects.filter(
|
||||||
|
workspace__slug=slug,
|
||||||
|
project__project_projectmember__member=request.user,
|
||||||
|
project__project_projectmember__is_active=True,
|
||||||
|
)
|
||||||
|
serializer = StateSerializer(states, many=True).data
|
||||||
|
return Response(serializer, status=status.HTTP_200_OK)
|
573
apiserver/plane/app/views/workspace/user.py
Normal file
573
apiserver/plane/app/views/workspace/user.py
Normal file
@ -0,0 +1,573 @@
|
|||||||
|
# Python imports
|
||||||
|
from datetime import date
|
||||||
|
from dateutil.relativedelta import relativedelta
|
||||||
|
|
||||||
|
# Django imports
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.db.models import (
|
||||||
|
OuterRef,
|
||||||
|
Func,
|
||||||
|
F,
|
||||||
|
Q,
|
||||||
|
Count,
|
||||||
|
Case,
|
||||||
|
Value,
|
||||||
|
CharField,
|
||||||
|
When,
|
||||||
|
Max,
|
||||||
|
IntegerField,
|
||||||
|
UUIDField,
|
||||||
|
)
|
||||||
|
from django.db.models.functions import ExtractWeek, Cast
|
||||||
|
from django.db.models.fields import DateField
|
||||||
|
from django.contrib.postgres.aggregates import ArrayAgg
|
||||||
|
from django.contrib.postgres.fields import ArrayField
|
||||||
|
from django.db.models.functions import Coalesce
|
||||||
|
|
||||||
|
# Third party modules
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.response import Response
|
||||||
|
|
||||||
|
# Module imports
|
||||||
|
from plane.app.serializers import (
|
||||||
|
WorkSpaceSerializer,
|
||||||
|
ProjectMemberSerializer,
|
||||||
|
IssueActivitySerializer,
|
||||||
|
IssueSerializer,
|
||||||
|
WorkspaceUserPropertiesSerializer,
|
||||||
|
)
|
||||||
|
from plane.app.views.base import BaseAPIView
|
||||||
|
from plane.db.models import (
|
||||||
|
User,
|
||||||
|
Workspace,
|
||||||
|
ProjectMember,
|
||||||
|
IssueActivity,
|
||||||
|
Issue,
|
||||||
|
IssueLink,
|
||||||
|
IssueAttachment,
|
||||||
|
IssueSubscriber,
|
||||||
|
Project,
|
||||||
|
WorkspaceMember,
|
||||||
|
CycleIssue,
|
||||||
|
WorkspaceUserProperties,
|
||||||
|
)
|
||||||
|
from plane.app.permissions import (
|
||||||
|
WorkspaceEntityPermission,
|
||||||
|
WorkspaceViewerPermission,
|
||||||
|
)
|
||||||
|
from plane.utils.issue_filters import issue_filters
|
||||||
|
|
||||||
|
|
||||||
|
class UserLastProjectWithWorkspaceEndpoint(BaseAPIView):
|
||||||
|
def get(self, request):
|
||||||
|
user = User.objects.get(pk=request.user.id)
|
||||||
|
|
||||||
|
last_workspace_id = user.last_workspace_id
|
||||||
|
|
||||||
|
if last_workspace_id is None:
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"project_details": [],
|
||||||
|
"workspace_details": {},
|
||||||
|
},
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
|
||||||
|
workspace = Workspace.objects.get(pk=last_workspace_id)
|
||||||
|
workspace_serializer = WorkSpaceSerializer(workspace)
|
||||||
|
|
||||||
|
project_member = ProjectMember.objects.filter(
|
||||||
|
workspace_id=last_workspace_id, member=request.user
|
||||||
|
).select_related("workspace", "project", "member", "workspace__owner")
|
||||||
|
|
||||||
|
project_member_serializer = ProjectMemberSerializer(
|
||||||
|
project_member, many=True
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"workspace_details": workspace_serializer.data,
|
||||||
|
"project_details": project_member_serializer.data,
|
||||||
|
},
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class WorkspaceUserProfileIssuesEndpoint(BaseAPIView):
|
||||||
|
permission_classes = [
|
||||||
|
WorkspaceViewerPermission,
|
||||||
|
]
|
||||||
|
|
||||||
|
def get(self, request, slug, user_id):
|
||||||
|
fields = [
|
||||||
|
field
|
||||||
|
for field in request.GET.get("fields", "").split(",")
|
||||||
|
if field
|
||||||
|
]
|
||||||
|
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.filter(
|
||||||
|
Q(assignees__in=[user_id])
|
||||||
|
| Q(created_by_id=user_id)
|
||||||
|
| Q(issue_subscribers__subscriber_id=user_id),
|
||||||
|
workspace__slug=slug,
|
||||||
|
project__project_projectmember__member=request.user,
|
||||||
|
project__project_projectmember__is_active=True
|
||||||
|
)
|
||||||
|
.filter(**filters)
|
||||||
|
.select_related("workspace", "project", "state", "parent")
|
||||||
|
.prefetch_related("assignees", "labels", "issue_module__module")
|
||||||
|
.annotate(cycle_id=F("issue_cycle__cycle_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")
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
sub_issues_count=Issue.issue_objects.filter(
|
||||||
|
parent=OuterRef("id")
|
||||||
|
)
|
||||||
|
.order_by()
|
||||||
|
.annotate(count=Func(F("id"), function="Count"))
|
||||||
|
.values("count")
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
label_ids=Coalesce(
|
||||||
|
ArrayAgg(
|
||||||
|
"labels__id",
|
||||||
|
distinct=True,
|
||||||
|
filter=~Q(labels__id__isnull=True),
|
||||||
|
),
|
||||||
|
Value([], output_field=ArrayField(UUIDField())),
|
||||||
|
),
|
||||||
|
assignee_ids=Coalesce(
|
||||||
|
ArrayAgg(
|
||||||
|
"assignees__id",
|
||||||
|
distinct=True,
|
||||||
|
filter=~Q(assignees__id__isnull=True),
|
||||||
|
),
|
||||||
|
Value([], output_field=ArrayField(UUIDField())),
|
||||||
|
),
|
||||||
|
module_ids=Coalesce(
|
||||||
|
ArrayAgg(
|
||||||
|
"issue_module__module_id",
|
||||||
|
distinct=True,
|
||||||
|
filter=~Q(issue_module__module_id__isnull=True),
|
||||||
|
),
|
||||||
|
Value([], output_field=ArrayField(UUIDField())),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.order_by("created_at")
|
||||||
|
).distinct()
|
||||||
|
|
||||||
|
# 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 = IssueSerializer(
|
||||||
|
issue_queryset, many=True, fields=fields if fields else None
|
||||||
|
).data
|
||||||
|
return Response(issues, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
|
class WorkspaceUserPropertiesEndpoint(BaseAPIView):
|
||||||
|
permission_classes = [
|
||||||
|
WorkspaceViewerPermission,
|
||||||
|
]
|
||||||
|
|
||||||
|
def patch(self, request, slug):
|
||||||
|
workspace_properties = WorkspaceUserProperties.objects.get(
|
||||||
|
user=request.user,
|
||||||
|
workspace__slug=slug,
|
||||||
|
)
|
||||||
|
|
||||||
|
workspace_properties.filters = request.data.get(
|
||||||
|
"filters", workspace_properties.filters
|
||||||
|
)
|
||||||
|
workspace_properties.display_filters = request.data.get(
|
||||||
|
"display_filters", workspace_properties.display_filters
|
||||||
|
)
|
||||||
|
workspace_properties.display_properties = request.data.get(
|
||||||
|
"display_properties", workspace_properties.display_properties
|
||||||
|
)
|
||||||
|
workspace_properties.save()
|
||||||
|
|
||||||
|
serializer = WorkspaceUserPropertiesSerializer(workspace_properties)
|
||||||
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
|
def get(self, request, slug):
|
||||||
|
(
|
||||||
|
workspace_properties,
|
||||||
|
_,
|
||||||
|
) = WorkspaceUserProperties.objects.get_or_create(
|
||||||
|
user=request.user, workspace__slug=slug
|
||||||
|
)
|
||||||
|
serializer = WorkspaceUserPropertiesSerializer(workspace_properties)
|
||||||
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
|
class WorkspaceUserProfileEndpoint(BaseAPIView):
|
||||||
|
def get(self, request, slug, user_id):
|
||||||
|
user_data = User.objects.get(pk=user_id)
|
||||||
|
|
||||||
|
requesting_workspace_member = WorkspaceMember.objects.get(
|
||||||
|
workspace__slug=slug,
|
||||||
|
member=request.user,
|
||||||
|
is_active=True,
|
||||||
|
)
|
||||||
|
projects = []
|
||||||
|
if requesting_workspace_member.role >= 10:
|
||||||
|
projects = (
|
||||||
|
Project.objects.filter(
|
||||||
|
workspace__slug=slug,
|
||||||
|
project_projectmember__member=request.user,
|
||||||
|
project_projectmember__is_active=True,
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
created_issues=Count(
|
||||||
|
"project_issue",
|
||||||
|
filter=Q(
|
||||||
|
project_issue__created_by_id=user_id,
|
||||||
|
project_issue__archived_at__isnull=True,
|
||||||
|
project_issue__is_draft=False,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
assigned_issues=Count(
|
||||||
|
"project_issue",
|
||||||
|
filter=Q(
|
||||||
|
project_issue__assignees__in=[user_id],
|
||||||
|
project_issue__archived_at__isnull=True,
|
||||||
|
project_issue__is_draft=False,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
completed_issues=Count(
|
||||||
|
"project_issue",
|
||||||
|
filter=Q(
|
||||||
|
project_issue__completed_at__isnull=False,
|
||||||
|
project_issue__assignees__in=[user_id],
|
||||||
|
project_issue__archived_at__isnull=True,
|
||||||
|
project_issue__is_draft=False,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.annotate(
|
||||||
|
pending_issues=Count(
|
||||||
|
"project_issue",
|
||||||
|
filter=Q(
|
||||||
|
project_issue__state__group__in=[
|
||||||
|
"backlog",
|
||||||
|
"unstarted",
|
||||||
|
"started",
|
||||||
|
],
|
||||||
|
project_issue__assignees__in=[user_id],
|
||||||
|
project_issue__archived_at__isnull=True,
|
||||||
|
project_issue__is_draft=False,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.values(
|
||||||
|
"id",
|
||||||
|
"logo_props",
|
||||||
|
"created_issues",
|
||||||
|
"assigned_issues",
|
||||||
|
"completed_issues",
|
||||||
|
"pending_issues",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"project_data": projects,
|
||||||
|
"user_data": {
|
||||||
|
"email": user_data.email,
|
||||||
|
"first_name": user_data.first_name,
|
||||||
|
"last_name": user_data.last_name,
|
||||||
|
"avatar": user_data.avatar,
|
||||||
|
"cover_image": user_data.cover_image,
|
||||||
|
"date_joined": user_data.date_joined,
|
||||||
|
"user_timezone": user_data.user_timezone,
|
||||||
|
"display_name": user_data.display_name,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
status=status.HTTP_200_OK,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class WorkspaceUserActivityEndpoint(BaseAPIView):
|
||||||
|
permission_classes = [
|
||||||
|
WorkspaceEntityPermission,
|
||||||
|
]
|
||||||
|
|
||||||
|
def get(self, request, slug, user_id):
|
||||||
|
projects = request.query_params.getlist("project", [])
|
||||||
|
|
||||||
|
queryset = IssueActivity.objects.filter(
|
||||||
|
~Q(field__in=["comment", "vote", "reaction", "draft"]),
|
||||||
|
workspace__slug=slug,
|
||||||
|
project__project_projectmember__member=request.user,
|
||||||
|
project__project_projectmember__is_active=True,
|
||||||
|
actor=user_id,
|
||||||
|
).select_related("actor", "workspace", "issue", "project")
|
||||||
|
|
||||||
|
if projects:
|
||||||
|
queryset = queryset.filter(project__in=projects)
|
||||||
|
|
||||||
|
return self.paginate(
|
||||||
|
request=request,
|
||||||
|
queryset=queryset,
|
||||||
|
on_results=lambda issue_activities: IssueActivitySerializer(
|
||||||
|
issue_activities, many=True
|
||||||
|
).data,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class WorkspaceUserProfileStatsEndpoint(BaseAPIView):
|
||||||
|
def get(self, request, slug, user_id):
|
||||||
|
filters = issue_filters(request.query_params, "GET")
|
||||||
|
|
||||||
|
state_distribution = (
|
||||||
|
Issue.issue_objects.filter(
|
||||||
|
workspace__slug=slug,
|
||||||
|
assignees__in=[user_id],
|
||||||
|
project__project_projectmember__member=request.user,
|
||||||
|
project__project_projectmember__is_active=True,
|
||||||
|
)
|
||||||
|
.filter(**filters)
|
||||||
|
.annotate(state_group=F("state__group"))
|
||||||
|
.values("state_group")
|
||||||
|
.annotate(state_count=Count("state_group"))
|
||||||
|
.order_by("state_group")
|
||||||
|
)
|
||||||
|
|
||||||
|
priority_order = ["urgent", "high", "medium", "low", "none"]
|
||||||
|
|
||||||
|
priority_distribution = (
|
||||||
|
Issue.issue_objects.filter(
|
||||||
|
workspace__slug=slug,
|
||||||
|
assignees__in=[user_id],
|
||||||
|
project__project_projectmember__member=request.user,
|
||||||
|
project__project_projectmember__is_active=True,
|
||||||
|
)
|
||||||
|
.filter(**filters)
|
||||||
|
.values("priority")
|
||||||
|
.annotate(priority_count=Count("priority"))
|
||||||
|
.filter(priority_count__gte=1)
|
||||||
|
.annotate(
|
||||||
|
priority_order=Case(
|
||||||
|
*[
|
||||||
|
When(priority=p, then=Value(i))
|
||||||
|
for i, p in enumerate(priority_order)
|
||||||
|
],
|
||||||
|
default=Value(len(priority_order)),
|
||||||
|
output_field=IntegerField(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.order_by("priority_order")
|
||||||
|
)
|
||||||
|
|
||||||
|
created_issues = (
|
||||||
|
Issue.issue_objects.filter(
|
||||||
|
workspace__slug=slug,
|
||||||
|
project__project_projectmember__member=request.user,
|
||||||
|
project__project_projectmember__is_active=True,
|
||||||
|
created_by_id=user_id,
|
||||||
|
)
|
||||||
|
.filter(**filters)
|
||||||
|
.count()
|
||||||
|
)
|
||||||
|
|
||||||
|
assigned_issues_count = (
|
||||||
|
Issue.issue_objects.filter(
|
||||||
|
workspace__slug=slug,
|
||||||
|
assignees__in=[user_id],
|
||||||
|
project__project_projectmember__member=request.user,
|
||||||
|
project__project_projectmember__is_active=True,
|
||||||
|
)
|
||||||
|
.filter(**filters)
|
||||||
|
.count()
|
||||||
|
)
|
||||||
|
|
||||||
|
pending_issues_count = (
|
||||||
|
Issue.issue_objects.filter(
|
||||||
|
~Q(state__group__in=["completed", "cancelled"]),
|
||||||
|
workspace__slug=slug,
|
||||||
|
assignees__in=[user_id],
|
||||||
|
project__project_projectmember__member=request.user,
|
||||||
|
project__project_projectmember__is_active=True,
|
||||||
|
)
|
||||||
|
.filter(**filters)
|
||||||
|
.count()
|
||||||
|
)
|
||||||
|
|
||||||
|
completed_issues_count = (
|
||||||
|
Issue.issue_objects.filter(
|
||||||
|
workspace__slug=slug,
|
||||||
|
assignees__in=[user_id],
|
||||||
|
state__group="completed",
|
||||||
|
project__project_projectmember__member=request.user,
|
||||||
|
project__project_projectmember__is_active=True,
|
||||||
|
)
|
||||||
|
.filter(**filters)
|
||||||
|
.count()
|
||||||
|
)
|
||||||
|
|
||||||
|
subscribed_issues_count = (
|
||||||
|
IssueSubscriber.objects.filter(
|
||||||
|
workspace__slug=slug,
|
||||||
|
subscriber_id=user_id,
|
||||||
|
project__project_projectmember__member=request.user,
|
||||||
|
project__project_projectmember__is_active=True,
|
||||||
|
)
|
||||||
|
.filter(**filters)
|
||||||
|
.count()
|
||||||
|
)
|
||||||
|
|
||||||
|
upcoming_cycles = CycleIssue.objects.filter(
|
||||||
|
workspace__slug=slug,
|
||||||
|
cycle__start_date__gt=timezone.now().date(),
|
||||||
|
issue__assignees__in=[
|
||||||
|
user_id,
|
||||||
|
],
|
||||||
|
).values("cycle__name", "cycle__id", "cycle__project_id")
|
||||||
|
|
||||||
|
present_cycle = CycleIssue.objects.filter(
|
||||||
|
workspace__slug=slug,
|
||||||
|
cycle__start_date__lt=timezone.now().date(),
|
||||||
|
cycle__end_date__gt=timezone.now().date(),
|
||||||
|
issue__assignees__in=[
|
||||||
|
user_id,
|
||||||
|
],
|
||||||
|
).values("cycle__name", "cycle__id", "cycle__project_id")
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
{
|
||||||
|
"state_distribution": state_distribution,
|
||||||
|
"priority_distribution": priority_distribution,
|
||||||
|
"created_issues": created_issues,
|
||||||
|
"assigned_issues": assigned_issues_count,
|
||||||
|
"completed_issues": completed_issues_count,
|
||||||
|
"pending_issues": pending_issues_count,
|
||||||
|
"subscribed_issues": subscribed_issues_count,
|
||||||
|
"present_cycles": present_cycle,
|
||||||
|
"upcoming_cycles": upcoming_cycles,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class UserActivityGraphEndpoint(BaseAPIView):
|
||||||
|
def get(self, request, slug):
|
||||||
|
issue_activities = (
|
||||||
|
IssueActivity.objects.filter(
|
||||||
|
actor=request.user,
|
||||||
|
workspace__slug=slug,
|
||||||
|
created_at__date__gte=date.today() + relativedelta(months=-6),
|
||||||
|
)
|
||||||
|
.annotate(created_date=Cast("created_at", DateField()))
|
||||||
|
.values("created_date")
|
||||||
|
.annotate(activity_count=Count("created_date"))
|
||||||
|
.order_by("created_date")
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response(issue_activities, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
|
class UserIssueCompletedGraphEndpoint(BaseAPIView):
|
||||||
|
def get(self, request, slug):
|
||||||
|
month = request.GET.get("month", 1)
|
||||||
|
|
||||||
|
issues = (
|
||||||
|
Issue.issue_objects.filter(
|
||||||
|
assignees__in=[request.user],
|
||||||
|
workspace__slug=slug,
|
||||||
|
completed_at__month=month,
|
||||||
|
completed_at__isnull=False,
|
||||||
|
)
|
||||||
|
.annotate(completed_week=ExtractWeek("completed_at"))
|
||||||
|
.annotate(week=F("completed_week") % 4)
|
||||||
|
.values("week")
|
||||||
|
.annotate(completed_count=Count("completed_week"))
|
||||||
|
.order_by("week")
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response(issues, status=status.HTTP_200_OK)
|
Loading…
Reference in New Issue
Block a user