ФУНКЦИИ - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: очистка stale assignees Operational Core

This commit is contained in:
DCCONSTRUCTIONS 2026-05-09 12:33:49 +03:00
parent fa4a533d29
commit 78deab1a23
10 changed files with 110 additions and 10 deletions

View File

@ -14,7 +14,7 @@ from drf_spectacular.utils import (
# Module imports
from .base import BaseAPIView
from plane.api.serializers import UserLiteSerializer, ProjectMemberSerializer
from plane.db.models import User, Workspace, WorkspaceMember, ProjectMember
from plane.db.models import IssueAssignee, User, Workspace, WorkspaceMember, ProjectMember
from plane.utils.permissions import ProjectMemberPermission, WorkSpaceAdminPermission, ProjectAdminPermission
from plane.utils.openapi import (
WORKSPACE_SLUG_PARAMETER,
@ -205,6 +205,8 @@ class ProjectMemberDetailAPIEndpoint(ProjectMemberListCreateAPIEndpoint):
serializer = ProjectMemberSerializer(project_member, data=request.data, partial=True, context={"slug": slug})
serializer.is_valid(raise_exception=True)
serializer.save()
if serializer.instance.is_active is False:
IssueAssignee.objects.filter(project_id=project_id, assignee_id=serializer.instance.member_id).delete()
return Response(serializer.data, status=status.HTTP_200_OK)
@extend_schema(
@ -219,4 +221,5 @@ class ProjectMemberDetailAPIEndpoint(ProjectMemberListCreateAPIEndpoint):
project_member = ProjectMember.objects.get(project_id=project_id, workspace__slug=slug, pk=pk)
project_member.is_active = False
project_member.save()
IssueAssignee.objects.filter(project_id=project_id, assignee_id=project_member.member_id).delete()
return Response(status=status.HTTP_204_NO_CONTENT)

View File

@ -570,7 +570,15 @@ class IssueViewSet(BaseViewSet):
Subquery(
IssueAssignee.objects.filter(
issue_id=OuterRef("pk"),
deleted_at__isnull=True,
assignee__is_active=True,
assignee__member_workspace__workspace_id=OuterRef("workspace_id"),
assignee__member_workspace__is_active=True,
assignee__member_workspace__is_banned=False,
assignee__member_workspace__deleted_at__isnull=True,
assignee__member_project__project_id=OuterRef("project_id"),
assignee__member_project__is_active=True,
assignee__member_project__deleted_at__isnull=True,
)
.values("issue_id")
.annotate(arr=ArrayAgg("assignee_id", distinct=True))
@ -676,7 +684,14 @@ class IssueViewSet(BaseViewSet):
distinct=True,
filter=Q(
~Q(assignees__id__isnull=True)
& Q(assignees__is_active=True)
& Q(assignees__member_workspace__workspace_id=F("workspace_id"))
& Q(assignees__member_workspace__is_active=True)
& Q(assignees__member_workspace__is_banned=False)
& Q(assignees__member_workspace__deleted_at__isnull=True)
& Q(assignees__member_project__project_id=F("project_id"))
& Q(assignees__member_project__is_active=True)
& Q(assignees__member_project__deleted_at__isnull=True)
& Q(issue_assignee__deleted_at__isnull=True)
),
),
@ -980,7 +995,15 @@ class IssuePaginatedViewSet(BaseViewSet):
Subquery(
IssueAssignee.objects.filter(
issue_id=OuterRef("pk"),
deleted_at__isnull=True,
assignee__is_active=True,
assignee__member_workspace__workspace_id=OuterRef("workspace_id"),
assignee__member_workspace__is_active=True,
assignee__member_workspace__is_banned=False,
assignee__member_workspace__deleted_at__isnull=True,
assignee__member_project__project_id=OuterRef("project_id"),
assignee__member_project__is_active=True,
assignee__member_project__deleted_at__isnull=True,
)
.values("issue_id")
.annotate(arr=ArrayAgg("assignee_id", distinct=True))
@ -1315,7 +1338,14 @@ class IssueDetailIdentifierEndpoint(BaseAPIView):
distinct=True,
filter=Q(
~Q(assignees__id__isnull=True)
& Q(assignees__is_active=True)
& Q(assignees__member_workspace__workspace_id=F("workspace_id"))
& Q(assignees__member_workspace__is_active=True)
& Q(assignees__member_workspace__is_banned=False)
& Q(assignees__member_workspace__deleted_at__isnull=True)
& Q(assignees__member_project__project_id=F("project_id"))
& Q(assignees__member_project__is_active=True)
& Q(assignees__member_project__deleted_at__isnull=True)
& Q(issue_assignee__deleted_at__isnull=True)
),
),

View File

@ -18,7 +18,7 @@ from plane.app.serializers import (
from plane.app.permissions import WorkspaceUserPermission
from plane.db.models import Project, ProjectMember, ProjectUserProperty, WorkspaceMember
from plane.db.models import IssueAssignee, Project, ProjectMember, ProjectUserProperty, WorkspaceMember
from plane.bgtasks.project_add_user_email_task import project_add_user_email
from plane.utils.host import base_host
from plane.app.permissions.base import allow_permission, ROLE
@ -295,6 +295,7 @@ class ProjectMemberViewSet(BaseViewSet):
project_member.is_active = False
project_member.save()
IssueAssignee.objects.filter(project_id=project_id, assignee_id=project_member.member_id).delete()
return Response(status=status.HTTP_204_NO_CONTENT)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
@ -323,6 +324,7 @@ class ProjectMemberViewSet(BaseViewSet):
# Deactivate the user
project_member.is_active = False
project_member.save()
IssueAssignee.objects.filter(project_id=project_id, assignee_id=project_member.member_id).delete()
return Response(status=status.HTTP_204_NO_CONTENT)

View File

@ -21,7 +21,7 @@ from plane.app.serializers import (
WorkSpaceMemberSerializer,
)
from plane.app.views.base import BaseAPIView
from plane.db.models import Project, ProjectMember, WorkspaceMember, DraftIssue
from plane.db.models import IssueAssignee, Project, ProjectMember, WorkspaceMember, DraftIssue
from plane.utils.cache import invalidate_cache
from .. import BaseViewSet
@ -144,6 +144,7 @@ class WorkSpaceMemberViewSet(BaseViewSet):
_ = ProjectMember.objects.filter(
workspace__slug=slug, member_id=workspace_member.member_id, is_active=True
).update(is_active=False, updated_at=timezone.now())
IssueAssignee.objects.filter(workspace__slug=slug, assignee_id=workspace_member.member_id).delete()
workspace_member.is_active = False
workspace_member.save()
@ -198,6 +199,7 @@ class WorkSpaceMemberViewSet(BaseViewSet):
_ = ProjectMember.objects.filter(
workspace__slug=slug, member_id=workspace_member.member_id, is_active=True
).update(is_active=False, updated_at=timezone.now())
IssueAssignee.objects.filter(workspace__slug=slug, assignee_id=workspace_member.member_id).delete()
# # Deactivate the user
workspace_member.is_active = False

View File

@ -9,7 +9,16 @@ from django.views import View
from django.views.decorators.csrf import csrf_exempt
from plane.authentication.views.nodedc_logout import is_internal_logout_request_authorized
from plane.db.models import ExternalIdentityLink, Profile, Project, ProjectMember, User, Workspace, WorkspaceMember
from plane.db.models import (
ExternalIdentityLink,
IssueAssignee,
Profile,
Project,
ProjectMember,
User,
Workspace,
WorkspaceMember,
)
OIDC_PROVIDER = "authentik"
@ -384,6 +393,10 @@ class NodeDCInternalWorkspaceMembershipRemoveEndpoint(View):
member=user,
is_active=True,
).update(is_active=False)
IssueAssignee.objects.filter(
workspace=workspace,
assignee=user,
).delete()
membership.is_active = False
membership.save(update_fields=["is_active", "updated_at"])
@ -534,6 +547,7 @@ class NodeDCInternalProjectMembershipRemoveEndpoint(View):
project_member.is_active = False
project_member.save(update_fields=["is_active", "updated_at"])
IssueAssignee.objects.filter(project=project, assignee=user).delete()
return JsonResponse(
{

View File

@ -14,6 +14,7 @@ from django.utils.dateparse import parse_datetime
from plane.app.views.base import BaseAPIView
from plane.license.api.permissions import InstanceAdminPermission
from plane.db.models import (
IssueAssignee,
Project,
ProjectMember,
Workspace,
@ -278,6 +279,7 @@ class InstanceWorkSpaceMemberEndpoint(BaseAPIView):
member_id=workspace_member.member_id,
is_active=True,
).update(is_active=False, updated_at=timezone.now())
IssueAssignee.objects.filter(workspace_id=workspace_id, assignee_id=workspace_member.member_id).delete()
workspace_member.is_active = False
workspace_member.save()
@ -332,6 +334,7 @@ class InstanceWorkSpaceMemberBanEndpoint(BaseAPIView):
member_id=workspace_member.member_id,
is_active=True,
).update(is_active=False, updated_at=timezone.now())
IssueAssignee.objects.filter(workspace_id=workspace_id, assignee_id=workspace_member.member_id).delete()
workspace_member.is_banned = True
workspace_member.banned_at = timezone.now()

View File

@ -33,8 +33,20 @@ def issue_queryset_grouper(
"module_ids": "issue_module__module_id",
}
valid_assignee_filter = (
Q(issue_assignee__deleted_at__isnull=True)
& Q(assignees__is_active=True)
& Q(assignees__member_workspace__workspace_id=F("workspace_id"))
& Q(assignees__member_workspace__is_active=True)
& Q(assignees__member_workspace__is_banned=False)
& Q(assignees__member_workspace__deleted_at__isnull=True)
& Q(assignees__member_project__project_id=F("project_id"))
& Q(assignees__member_project__is_active=True)
& Q(assignees__member_project__deleted_at__isnull=True)
)
GROUP_FILTER_MAPPER = {
"assignees__id": Q(issue_assignee__deleted_at__isnull=True),
"assignees__id": valid_assignee_filter,
"labels__id": Q(label_issue__deleted_at__isnull=True),
"issue_module__module_id": Q(issue_module__deleted_at__isnull=True),
}
@ -46,7 +58,7 @@ def issue_queryset_grouper(
annotations_map = {
"assignee_ids": (
"assignees__id",
~Q(assignees__id__isnull=True) & Q(issue_assignee__deleted_at__isnull=True),
~Q(assignees__id__isnull=True) & valid_assignee_filter,
),
"label_ids": (
"labels__id",

View File

@ -625,7 +625,14 @@ class IssueRetrievePublicEndpoint(BaseAPIView):
distinct=True,
filter=Q(
~Q(assignees__id__isnull=True)
& Q(assignees__is_active=True)
& Q(assignees__member_workspace__workspace_id=F("workspace_id"))
& Q(assignees__member_workspace__is_active=True)
& Q(assignees__member_workspace__is_banned=False)
& Q(assignees__member_workspace__deleted_at__isnull=True)
& Q(assignees__member_project__project_id=F("project_id"))
& Q(assignees__member_project__is_active=True)
& Q(assignees__member_project__deleted_at__isnull=True)
& Q(issue_assignee__deleted_at__isnull=True)
),
),

View File

@ -5,7 +5,7 @@
# Django imports
from django.contrib.postgres.aggregates import ArrayAgg
from django.contrib.postgres.fields import ArrayField
from django.db.models import Q, UUIDField, Value, QuerySet, OuterRef, Subquery
from django.db.models import F, Q, UUIDField, Value, QuerySet, OuterRef, Subquery
from django.db.models.functions import Coalesce
# Module imports
@ -37,8 +37,20 @@ def issue_queryset_grouper(
"module_ids": "issue_module__module_id",
}
valid_assignee_filter = (
Q(issue_assignee__deleted_at__isnull=True)
& Q(assignees__is_active=True)
& Q(assignees__member_workspace__workspace_id=F("workspace_id"))
& Q(assignees__member_workspace__is_active=True)
& Q(assignees__member_workspace__is_banned=False)
& Q(assignees__member_workspace__deleted_at__isnull=True)
& Q(assignees__member_project__project_id=F("project_id"))
& Q(assignees__member_project__is_active=True)
& Q(assignees__member_project__deleted_at__isnull=True)
)
GROUP_FILTER_MAPPER: Dict[str, Q] = {
"assignees__id": Q(issue_assignee__deleted_at__isnull=True),
"assignees__id": valid_assignee_filter,
"labels__id": Q(label_issue__deleted_at__isnull=True),
"issue_module__module_id": Q(issue_module__deleted_at__isnull=True),
}
@ -51,6 +63,14 @@ def issue_queryset_grouper(
IssueAssignee.objects.filter(
issue_id=OuterRef("pk"),
deleted_at__isnull=True,
assignee__is_active=True,
assignee__member_workspace__workspace_id=OuterRef("workspace_id"),
assignee__member_workspace__is_active=True,
assignee__member_workspace__is_banned=False,
assignee__member_workspace__deleted_at__isnull=True,
assignee__member_project__project_id=OuterRef("project_id"),
assignee__member_project__is_active=True,
assignee__member_project__deleted_at__isnull=True,
)
.values("issue_id")
.annotate(arr=ArrayAgg("assignee_id", distinct=True))

View File

@ -46,7 +46,10 @@ export const InternalContourKanbanCard = observer(function InternalContourKanban
const { t } = useTranslation();
const { isMobile } = usePlatformOS();
const { workspaceSlug: routerWorkspaceSlug } = useParams();
const { getUserDetails } = useMember();
const {
getUserDetails,
project: { getProjectMemberIds },
} = useMember();
const { getProjectById } = useProject();
const { getStateById, getProjectStateIds } = useProjectState();
@ -86,7 +89,11 @@ export const InternalContourKanbanCard = observer(function InternalContourKanban
const checkerItemsTotal = issue.checker_items_count ?? 0;
const checkerItemsCompleted = issue.checker_items_completed_count ?? 0;
const hasCheckerProgress = checkerBlocksTotal > 0;
const assigneeIds = issue.assignee_ids ?? [];
const rawAssigneeIds = issue.assignee_ids ?? [];
const projectMemberIds = issue.project_id ? getProjectMemberIds(issue.project_id, true) : null;
const assigneeIds = projectMemberIds
? rawAssigneeIds.filter((assigneeId) => projectMemberIds.includes(assigneeId))
: rawAssigneeIds;
const visibleAssigneeIds = assigneeIds.slice(0, 3);
const assigneeCount = assigneeIds.length;
const assigneeStackWidthClass =