ФУНКЦИИ - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: guest-доступ Operational Core
This commit is contained in:
parent
d0e2f423e6
commit
87e1857f53
|
|
@ -0,0 +1,15 @@
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
def nodedc_env_flag(name):
|
||||||
|
return os.environ.get(name, "").strip().lower() in {"1", "true", "yes", "on"}
|
||||||
|
|
||||||
|
|
||||||
|
def nodedc_guest_read_all_issues_enabled():
|
||||||
|
return nodedc_env_flag("PLANE_NODEDC_GUEST_READ_ALL_ISSUES") or nodedc_env_flag(
|
||||||
|
"PLANE_NODEDC_ACCESS_ENFORCEMENT"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def should_limit_guest_to_own_issues(project):
|
||||||
|
return not project.guest_view_all_features and not nodedc_guest_read_all_issues_enabled()
|
||||||
|
|
@ -34,7 +34,7 @@ class IssueAttachmentEndpoint(BaseAPIView):
|
||||||
model = FileAsset
|
model = FileAsset
|
||||||
parser_classes = (MultiPartParser, FormParser)
|
parser_classes = (MultiPartParser, FormParser)
|
||||||
|
|
||||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||||
def post(self, request, slug, project_id, issue_id):
|
def post(self, request, slug, project_id, issue_id):
|
||||||
serializer = IssueAttachmentSerializer(data=request.data)
|
serializer = IssueAttachmentSerializer(data=request.data)
|
||||||
workspace = Workspace.objects.get(slug=slug)
|
workspace = Workspace.objects.get(slug=slug)
|
||||||
|
|
@ -104,7 +104,7 @@ class IssueAttachmentV2Endpoint(BaseAPIView):
|
||||||
serializer_class = IssueAttachmentSerializer
|
serializer_class = IssueAttachmentSerializer
|
||||||
model = FileAsset
|
model = FileAsset
|
||||||
|
|
||||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||||
def post(self, request, slug, project_id, issue_id):
|
def post(self, request, slug, project_id, issue_id):
|
||||||
name = request.data.get("name")
|
name = request.data.get("name")
|
||||||
type = request.data.get("type", False)
|
type = request.data.get("type", False)
|
||||||
|
|
@ -209,7 +209,7 @@ class IssueAttachmentV2Endpoint(BaseAPIView):
|
||||||
serializer = IssueAttachmentSerializer(issue_attachments, many=True)
|
serializer = IssueAttachmentSerializer(issue_attachments, many=True)
|
||||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||||
def patch(self, request, slug, project_id, issue_id, pk):
|
def patch(self, request, slug, project_id, issue_id, pk):
|
||||||
issue_attachment = FileAsset.objects.get(pk=pk, workspace__slug=slug, project_id=project_id)
|
issue_attachment = FileAsset.objects.get(pk=pk, workspace__slug=slug, project_id=project_id)
|
||||||
serializer = IssueAttachmentSerializer(issue_attachment)
|
serializer = IssueAttachmentSerializer(issue_attachment)
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,10 @@ from rest_framework.response import Response
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from plane.app.permissions import ROLE, allow_permission
|
from plane.app.permissions import ROLE, allow_permission
|
||||||
|
from plane.app.nodedc_access import (
|
||||||
|
nodedc_guest_read_all_issues_enabled,
|
||||||
|
should_limit_guest_to_own_issues,
|
||||||
|
)
|
||||||
from plane.app.realtime.issue_events import publish_issue_event_on_commit
|
from plane.app.realtime.issue_events import publish_issue_event_on_commit
|
||||||
from plane.app.serializers import (
|
from plane.app.serializers import (
|
||||||
IssueCreateSerializer,
|
IssueCreateSerializer,
|
||||||
|
|
@ -333,7 +337,7 @@ class IssueViewSet(BaseViewSet):
|
||||||
role=5,
|
role=5,
|
||||||
is_active=True,
|
is_active=True,
|
||||||
).exists()
|
).exists()
|
||||||
and not project.guest_view_all_features
|
and should_limit_guest_to_own_issues(project)
|
||||||
):
|
):
|
||||||
issue_queryset = issue_queryset.filter(created_by=request.user)
|
issue_queryset = issue_queryset.filter(created_by=request.user)
|
||||||
filtered_issue_queryset = filtered_issue_queryset.filter(created_by=request.user)
|
filtered_issue_queryset = filtered_issue_queryset.filter(created_by=request.user)
|
||||||
|
|
@ -641,7 +645,7 @@ class IssueViewSet(BaseViewSet):
|
||||||
role=5,
|
role=5,
|
||||||
is_active=True,
|
is_active=True,
|
||||||
).exists()
|
).exists()
|
||||||
and not project.guest_view_all_features
|
and should_limit_guest_to_own_issues(project)
|
||||||
and not issue.created_by == request.user
|
and not issue.created_by == request.user
|
||||||
):
|
):
|
||||||
return Response(
|
return Response(
|
||||||
|
|
@ -972,7 +976,7 @@ class IssuePaginatedViewSet(BaseViewSet):
|
||||||
role=5,
|
role=5,
|
||||||
is_active=True,
|
is_active=True,
|
||||||
)
|
)
|
||||||
if project_member.exists() and not project.guest_view_all_features:
|
if project_member.exists() and should_limit_guest_to_own_issues(project):
|
||||||
base_queryset = base_queryset.filter(created_by=request.user)
|
base_queryset = base_queryset.filter(created_by=request.user)
|
||||||
queryset = queryset.filter(created_by=request.user)
|
queryset = queryset.filter(created_by=request.user)
|
||||||
|
|
||||||
|
|
@ -1093,8 +1097,17 @@ class IssueDetailEndpoint(BaseAPIView):
|
||||||
def get(self, request, slug, project_id):
|
def get(self, request, slug, project_id):
|
||||||
filters = issue_filters(request.query_params, "GET")
|
filters = issue_filters(request.query_params, "GET")
|
||||||
|
|
||||||
# check for the project member role, if the role is 5 then check for the guest_view_all_features
|
guest_read_filter = Q(
|
||||||
# if it is true then show all the issues else show only the issues created by the user
|
project__project_projectmember__member=self.request.user,
|
||||||
|
project__project_projectmember__is_active=True,
|
||||||
|
project__project_projectmember__role=ROLE.GUEST.value,
|
||||||
|
)
|
||||||
|
if not nodedc_guest_read_all_issues_enabled():
|
||||||
|
guest_read_filter = guest_read_filter & (
|
||||||
|
Q(project__guest_view_all_features=True)
|
||||||
|
| Q(project__guest_view_all_features=False, created_by=self.request.user)
|
||||||
|
)
|
||||||
|
|
||||||
permission_subquery = (
|
permission_subquery = (
|
||||||
Issue.issue_objects.filter(workspace__slug=slug, project_id=project_id, id=OuterRef("id"))
|
Issue.issue_objects.filter(workspace__slug=slug, project_id=project_id, id=OuterRef("id"))
|
||||||
.filter(
|
.filter(
|
||||||
|
|
@ -1103,19 +1116,7 @@ class IssueDetailEndpoint(BaseAPIView):
|
||||||
project__project_projectmember__is_active=True,
|
project__project_projectmember__is_active=True,
|
||||||
project__project_projectmember__role__gt=ROLE.GUEST.value,
|
project__project_projectmember__role__gt=ROLE.GUEST.value,
|
||||||
)
|
)
|
||||||
| Q(
|
| guest_read_filter
|
||||||
project__project_projectmember__member=self.request.user,
|
|
||||||
project__project_projectmember__is_active=True,
|
|
||||||
project__project_projectmember__role=ROLE.GUEST.value,
|
|
||||||
project__guest_view_all_features=True,
|
|
||||||
)
|
|
||||||
| Q(
|
|
||||||
project__project_projectmember__member=self.request.user,
|
|
||||||
project__project_projectmember__is_active=True,
|
|
||||||
project__project_projectmember__role=ROLE.GUEST.value,
|
|
||||||
project__guest_view_all_features=False,
|
|
||||||
created_by=self.request.user,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
.values("id")
|
.values("id")
|
||||||
)
|
)
|
||||||
|
|
@ -1418,7 +1419,7 @@ class IssueDetailIdentifierEndpoint(BaseAPIView):
|
||||||
role=5,
|
role=5,
|
||||||
is_active=True,
|
is_active=True,
|
||||||
).exists()
|
).exists()
|
||||||
and not project.guest_view_all_features
|
and should_limit_guest_to_own_issues(project)
|
||||||
and not issue.created_by == request.user
|
and not issue.created_by == request.user
|
||||||
):
|
):
|
||||||
return Response(
|
return Response(
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ class IssueCommentViewSet(BaseViewSet):
|
||||||
.distinct()
|
.distinct()
|
||||||
)
|
)
|
||||||
|
|
||||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||||
def create(self, request, slug, project_id, issue_id):
|
def create(self, request, slug, project_id, issue_id):
|
||||||
project = Project.objects.get(pk=project_id)
|
project = Project.objects.get(pk=project_id)
|
||||||
issue = Issue.objects.get(pk=issue_id)
|
issue = Issue.objects.get(pk=issue_id)
|
||||||
|
|
@ -180,7 +180,7 @@ class CommentReactionViewSet(BaseViewSet):
|
||||||
.distinct()
|
.distinct()
|
||||||
)
|
)
|
||||||
|
|
||||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||||
def create(self, request, slug, project_id, comment_id):
|
def create(self, request, slug, project_id, comment_id):
|
||||||
try:
|
try:
|
||||||
serializer = CommentReactionSerializer(data=request.data)
|
serializer = CommentReactionSerializer(data=request.data)
|
||||||
|
|
@ -209,7 +209,7 @@ class CommentReactionViewSet(BaseViewSet):
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||||
def destroy(self, request, slug, project_id, comment_id, reaction_code):
|
def destroy(self, request, slug, project_id, comment_id, reaction_code):
|
||||||
comment_reaction = CommentReaction.objects.get(
|
comment_reaction = CommentReaction.objects.get(
|
||||||
workspace__slug=slug,
|
workspace__slug=slug,
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ class IssueReactionViewSet(BaseViewSet):
|
||||||
.distinct()
|
.distinct()
|
||||||
)
|
)
|
||||||
|
|
||||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||||
def create(self, request, slug, project_id, issue_id):
|
def create(self, request, slug, project_id, issue_id):
|
||||||
serializer = IssueReactionSerializer(data=request.data)
|
serializer = IssueReactionSerializer(data=request.data)
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
|
|
@ -61,7 +61,7 @@ class IssueReactionViewSet(BaseViewSet):
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
|
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
|
||||||
def destroy(self, request, slug, project_id, issue_id, reaction_code):
|
def destroy(self, request, slug, project_id, issue_id, reaction_code):
|
||||||
issue_reaction = IssueReaction.objects.get(
|
issue_reaction = IssueReaction.objects.get(
|
||||||
workspace__slug=slug,
|
workspace__slug=slug,
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ from plane.app.serializers import (
|
||||||
IssueDescriptionVersionDetailSerializer,
|
IssueDescriptionVersionDetailSerializer,
|
||||||
)
|
)
|
||||||
from plane.app.permissions import allow_permission, ROLE
|
from plane.app.permissions import allow_permission, ROLE
|
||||||
|
from plane.app.nodedc_access import should_limit_guest_to_own_issues
|
||||||
from plane.utils.global_paginator import paginate
|
from plane.utils.global_paginator import paginate
|
||||||
from plane.utils.timezone_converter import user_timezone_converter
|
from plane.utils.timezone_converter import user_timezone_converter
|
||||||
|
|
||||||
|
|
@ -96,7 +97,7 @@ class WorkItemDescriptionVersionEndpoint(BaseAPIView):
|
||||||
role=ROLE.GUEST.value,
|
role=ROLE.GUEST.value,
|
||||||
is_active=True,
|
is_active=True,
|
||||||
).exists()
|
).exists()
|
||||||
and not project.guest_view_all_features
|
and should_limit_guest_to_own_issues(project)
|
||||||
and not issue.created_by == request.user
|
and not issue.created_by == request.user
|
||||||
):
|
):
|
||||||
return Response(
|
return Response(
|
||||||
|
|
|
||||||
|
|
@ -120,10 +120,14 @@ export const BaseCalendarRoot = observer(function BaseCalendarRoot(props: IBaseC
|
||||||
issueProjectId,
|
issueProjectId,
|
||||||
updateIssue
|
updateIssue
|
||||||
).catch((err) => {
|
).catch((err) => {
|
||||||
|
const message =
|
||||||
|
err?.detail === "You are not allowed to move this work item"
|
||||||
|
? "У вас нет прав перемещать эту карточку"
|
||||||
|
: "Не удалось выполнить действие";
|
||||||
setToast({
|
setToast({
|
||||||
title: "Error!",
|
title: "Ошибка",
|
||||||
type: TOAST_TYPE.ERROR,
|
type: TOAST_TYPE.ERROR,
|
||||||
message: err?.detail ?? "Failed to perform this action",
|
message,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -103,7 +103,7 @@ export const BaseGanttRoot = observer(function BaseGanttRoot(props: IBaseGanttRo
|
||||||
setToast({
|
setToast({
|
||||||
type: TOAST_TYPE.ERROR,
|
type: TOAST_TYPE.ERROR,
|
||||||
title: t("toast.error"),
|
title: t("toast.error"),
|
||||||
message: "Error while updating work item dates, Please try again Later",
|
message: "Не удалось обновить даты карточки. Попробуйте позже.",
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
[issues, projectId, workspaceSlug]
|
[issues, projectId, workspaceSlug]
|
||||||
|
|
|
||||||
|
|
@ -305,10 +305,10 @@ export const KanbanIssueBlock = observer(function KanbanIssueBlock(props: IssueB
|
||||||
else {
|
else {
|
||||||
setToast({
|
setToast({
|
||||||
type: TOAST_TYPE.WARNING,
|
type: TOAST_TYPE.WARNING,
|
||||||
title: "Cannot move work item",
|
title: "Нельзя переместить карточку",
|
||||||
message: !canEditIssueProperties
|
message: !canEditIssueProperties
|
||||||
? "You are not allowed to move this work item"
|
? "У вас нет прав перемещать эту карточку"
|
||||||
: "Drag and drop is disabled for the current grouping",
|
: "Перетаскивание отключено для текущей группировки",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -243,10 +243,10 @@ export const IssueBlock = observer(function IssueBlock(props: IssueBlockProps) {
|
||||||
if (!isDraggingAllowed) {
|
if (!isDraggingAllowed) {
|
||||||
setToast({
|
setToast({
|
||||||
type: TOAST_TYPE.WARNING,
|
type: TOAST_TYPE.WARNING,
|
||||||
title: "Cannot move work item",
|
title: "Нельзя переместить карточку",
|
||||||
message: !canEditIssueProperties
|
message: !canEditIssueProperties
|
||||||
? "You are not allowed to move this work item"
|
? "У вас нет прав перемещать эту карточку"
|
||||||
: "Drag and drop is disabled for the current grouping",
|
: "Перетаскивание отключено для текущей группировки",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -183,7 +183,7 @@ const getCycleColumns = (): IGroupByColumn[] | undefined => {
|
||||||
icon: <CycleGroupIcon cycleGroup={cycleStatus} className="h-3.5 w-3.5" />,
|
icon: <CycleGroupIcon cycleGroup={cycleStatus} className="h-3.5 w-3.5" />,
|
||||||
payload: { cycle_id: cycle.id },
|
payload: { cycle_id: cycle.id },
|
||||||
isDropDisabled,
|
isDropDisabled,
|
||||||
dropErrorMessage: isDropDisabled ? "Work item cannot be moved to completed cycles" : undefined,
|
dropErrorMessage: isDropDisabled ? "Карточку нельзя перенести в завершённый цикл" : undefined,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
cycles.push({
|
cycles.push({
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,13 @@ type DNDStoreType =
|
||||||
| EIssuesStoreType.EPIC
|
| EIssuesStoreType.EPIC
|
||||||
| EIssuesStoreType.TEAM_PROJECT_WORK_ITEMS;
|
| EIssuesStoreType.TEAM_PROJECT_WORK_ITEMS;
|
||||||
|
|
||||||
|
const resolveDragDropErrorMessage = (message?: string) => {
|
||||||
|
if (message === "You are not allowed to move this work item") return "У вас нет прав перемещать эту карточку";
|
||||||
|
if (message === "Work item cannot be moved to completed cycles") return "Карточку нельзя перенести в завершённый цикл";
|
||||||
|
if (message === "Failed to perform this action") return "Не удалось выполнить действие";
|
||||||
|
return message || "Не удалось выполнить действие";
|
||||||
|
};
|
||||||
|
|
||||||
export const useGroupIssuesDragNDrop = (
|
export const useGroupIssuesDragNDrop = (
|
||||||
storeType: DNDStoreType,
|
storeType: DNDStoreType,
|
||||||
orderBy: TIssueOrderByOptions | undefined,
|
orderBy: TIssueOrderByOptions | undefined,
|
||||||
|
|
@ -63,8 +70,8 @@ export const useGroupIssuesDragNDrop = (
|
||||||
) => {
|
) => {
|
||||||
const errorToastProps = {
|
const errorToastProps = {
|
||||||
type: TOAST_TYPE.ERROR,
|
type: TOAST_TYPE.ERROR,
|
||||||
title: "Error!",
|
title: "Ошибка",
|
||||||
message: "Error while updating work item",
|
message: "Не удалось обновить карточку",
|
||||||
};
|
};
|
||||||
const moduleKey = ISSUE_FILTER_DEFAULT_DATA["module"];
|
const moduleKey = ISSUE_FILTER_DEFAULT_DATA["module"];
|
||||||
const cycleKey = ISSUE_FILTER_DEFAULT_DATA["cycle"];
|
const cycleKey = ISSUE_FILTER_DEFAULT_DATA["cycle"];
|
||||||
|
|
@ -117,9 +124,9 @@ export const useGroupIssuesDragNDrop = (
|
||||||
orderBy !== "sort_order"
|
orderBy !== "sort_order"
|
||||||
).catch((err) => {
|
).catch((err) => {
|
||||||
setToast({
|
setToast({
|
||||||
title: "Error!",
|
title: "Ошибка",
|
||||||
type: TOAST_TYPE.ERROR,
|
type: TOAST_TYPE.ERROR,
|
||||||
message: err?.detail ?? "Failed to perform this action",
|
message: resolveDragDropErrorMessage(err?.detail),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue