ФУНКЦИИ - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: блокировка и удаление участников воркспейса в God Mode

This commit is contained in:
DCCONSTRUCTIONS
2026-04-29 01:15:17 +03:00
parent 7bf416ec1f
commit 4ba3aab02e
19 changed files with 598 additions and 42 deletions
@@ -26,5 +26,6 @@ from .workspace import (
InstanceWorkSpaceAvailabilityCheckEndpoint,
InstanceWorkSpaceEndpoint,
InstanceWorkSpaceFeatureEndpoint,
InstanceWorkSpaceMemberBanEndpoint,
InstanceWorkSpaceMemberEndpoint,
)
@@ -8,6 +8,7 @@ from rest_framework import status
from django.db import IntegrityError
from django.db.models import Count, OuterRef, Func, F, Q
from django.utils import timezone
from django.utils.dateparse import parse_datetime
# Module imports
from plane.app.views.base import BaseAPIView
@@ -23,6 +24,7 @@ from plane.db.models import (
)
from plane.license.api.serializers import WorkspaceSerializer
from plane.utils.constants import RESTRICTED_WORKSPACE_SLUGS
from plane.utils.workspace_bans import release_expired_workspace_bans, release_workspace_member_ban
WORKSPACE_ROLES = {5, 15, 20}
@@ -46,6 +48,9 @@ def serialize_instance_workspace_member(workspace_member):
},
"role": workspace_member.role,
"is_active": workspace_member.is_active,
"is_banned": workspace_member.is_banned,
"banned_at": workspace_member.banned_at,
"banned_until": workspace_member.banned_until,
"created_at": workspace_member.created_at,
"active_project_count": getattr(workspace_member, "active_project_count", 0),
"admin_project_count": getattr(workspace_member, "admin_project_count", 0),
@@ -57,6 +62,7 @@ def has_other_workspace_admin(workspace_member):
workspace=workspace_member.workspace,
role=20,
is_active=True,
is_banned=False,
member__is_bot=False,
).exclude(pk=workspace_member.pk).exists()
@@ -87,8 +93,10 @@ def is_only_project_admin(workspace_member):
def get_workspace_member_queryset(workspace_id):
release_expired_workspace_bans(workspace_id=workspace_id)
return (
WorkspaceMember.objects.filter(workspace_id=workspace_id, member__is_bot=False, is_active=True)
WorkspaceMember.objects.filter(workspace_id=workspace_id, member__is_bot=False)
.filter(Q(is_active=True) | Q(is_banned=True))
.select_related("workspace", "member", "member__avatar_asset")
.annotate(
active_project_count=Count(
@@ -276,6 +284,77 @@ class InstanceWorkSpaceMemberEndpoint(BaseAPIView):
return Response(status=status.HTTP_204_NO_CONTENT)
class InstanceWorkSpaceMemberBanEndpoint(BaseAPIView):
permission_classes = [InstanceAdminPermission]
def patch(self, request, workspace_id, member_id):
workspace_member = get_workspace_member_queryset(workspace_id).filter(pk=member_id).first()
if not workspace_member:
return Response({"error": "Workspace member not found"}, status=status.HTTP_404_NOT_FOUND)
is_banned = request.data.get("is_banned")
if not isinstance(is_banned, bool):
return Response({"error": "is_banned must be a boolean"}, status=status.HTTP_400_BAD_REQUEST)
banned_until = None
if is_banned:
raw_banned_until = request.data.get("banned_until")
if raw_banned_until not in (None, ""):
banned_until = parse_datetime(str(raw_banned_until))
if banned_until is None:
return Response({"error": "banned_until must be an ISO datetime"}, status=status.HTTP_400_BAD_REQUEST)
if timezone.is_naive(banned_until):
banned_until = timezone.make_aware(banned_until, timezone.get_current_timezone())
if banned_until <= timezone.now():
return Response({"error": "banned_until must be in the future"}, status=status.HTTP_400_BAD_REQUEST)
if workspace_member.role == 20 and not has_other_workspace_admin(workspace_member):
return Response(
{"error": "Cannot ban the only workspace admin"},
status=status.HTTP_400_BAD_REQUEST,
)
if is_only_project_admin(workspace_member):
return Response(
{"error": "Cannot ban a member who is the only admin in one or more projects"},
status=status.HTTP_400_BAD_REQUEST,
)
project_member_ids = list(
ProjectMember.objects.filter(
workspace_id=workspace_id,
member_id=workspace_member.member_id,
is_active=True,
).values_list("id", flat=True)
)
ProjectMember.objects.filter(
workspace_id=workspace_id,
member_id=workspace_member.member_id,
is_active=True,
).update(is_active=False, updated_at=timezone.now())
workspace_member.is_banned = True
workspace_member.banned_at = timezone.now()
workspace_member.banned_until = banned_until
workspace_member.ban_project_member_ids = [str(project_member_id) for project_member_id in project_member_ids]
workspace_member.is_active = False
workspace_member.save(
update_fields=[
"is_banned",
"banned_at",
"banned_until",
"ban_project_member_ids",
"is_active",
"updated_at",
]
)
else:
release_workspace_member_ban(workspace_member)
workspace_member = get_workspace_member_queryset(workspace_id).get(pk=member_id)
return Response(serialize_instance_workspace_member(workspace_member), status=status.HTTP_200_OK)
class InstanceWorkSpaceFeatureEndpoint(BaseAPIView):
permission_classes = [InstanceAdminPermission]
+6
View File
@@ -19,6 +19,7 @@ from plane.license.api.views import (
InstanceWorkSpaceAvailabilityCheckEndpoint,
InstanceWorkSpaceEndpoint,
InstanceWorkSpaceFeatureEndpoint,
InstanceWorkSpaceMemberBanEndpoint,
InstanceWorkSpaceMemberEndpoint,
)
@@ -83,6 +84,11 @@ urlpatterns = [
InstanceWorkSpaceMemberEndpoint.as_view(),
name="instance-workspace-member",
),
path(
"workspaces/<uuid:workspace_id>/members/<uuid:member_id>/ban/",
InstanceWorkSpaceMemberBanEndpoint.as_view(),
name="instance-workspace-member-ban",
),
path(
"workspaces/<uuid:workspace_id>/features/",
InstanceWorkSpaceFeatureEndpoint.as_view(),