From 78deab1a23c7e8b805aaa141ba2abf0878fb2a6e Mon Sep 17 00:00:00 2001 From: DCCONSTRUCTIONS Date: Sat, 9 May 2026 12:33:49 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A4=D0=A3=D0=9D=D0=9A=D0=A6=D0=98=D0=98=20-?= =?UTF-8?q?=20=D0=9C=D0=95=D0=96=D0=9F=D0=A0=D0=9E=D0=95=D0=9A=D0=A2=D0=9D?= =?UTF-8?q?=D0=90=D0=AF=20=D0=9A=D0=9E=D0=9C=D0=9C=D0=A3=D0=9D=D0=98=D0=9A?= =?UTF-8?q?=D0=90=D0=A6=D0=98=D0=AF:=20=D0=BE=D1=87=D0=B8=D1=81=D1=82?= =?UTF-8?q?=D0=BA=D0=B0=20stale=20assignees=20Operational=20Core?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plane-src/apps/api/plane/api/views/member.py | 5 +++- .../apps/api/plane/app/views/issue/base.py | 30 +++++++++++++++++++ .../api/plane/app/views/project/member.py | 4 ++- .../api/plane/app/views/workspace/member.py | 4 ++- .../views/nodedc_workspace_adapter.py | 16 +++++++++- .../api/plane/license/api/views/workspace.py | 3 ++ .../apps/api/plane/space/utils/grouper.py | 16 ++++++++-- plane-src/apps/api/plane/space/views/issue.py | 7 +++++ plane-src/apps/api/plane/utils/grouper.py | 24 +++++++++++++-- .../kanban/internal-contour-card.tsx | 11 +++++-- 10 files changed, 110 insertions(+), 10 deletions(-) diff --git a/plane-src/apps/api/plane/api/views/member.py b/plane-src/apps/api/plane/api/views/member.py index adb28be..621ddca 100644 --- a/plane-src/apps/api/plane/api/views/member.py +++ b/plane-src/apps/api/plane/api/views/member.py @@ -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) diff --git a/plane-src/apps/api/plane/app/views/issue/base.py b/plane-src/apps/api/plane/app/views/issue/base.py index 3efcfa8..86128a5 100644 --- a/plane-src/apps/api/plane/app/views/issue/base.py +++ b/plane-src/apps/api/plane/app/views/issue/base.py @@ -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) ), ), diff --git a/plane-src/apps/api/plane/app/views/project/member.py b/plane-src/apps/api/plane/app/views/project/member.py index 7dfe709..faafb4e 100644 --- a/plane-src/apps/api/plane/app/views/project/member.py +++ b/plane-src/apps/api/plane/app/views/project/member.py @@ -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) diff --git a/plane-src/apps/api/plane/app/views/workspace/member.py b/plane-src/apps/api/plane/app/views/workspace/member.py index 67c7637..96f3423 100644 --- a/plane-src/apps/api/plane/app/views/workspace/member.py +++ b/plane-src/apps/api/plane/app/views/workspace/member.py @@ -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 diff --git a/plane-src/apps/api/plane/authentication/views/nodedc_workspace_adapter.py b/plane-src/apps/api/plane/authentication/views/nodedc_workspace_adapter.py index a0e5541..1812734 100644 --- a/plane-src/apps/api/plane/authentication/views/nodedc_workspace_adapter.py +++ b/plane-src/apps/api/plane/authentication/views/nodedc_workspace_adapter.py @@ -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( { diff --git a/plane-src/apps/api/plane/license/api/views/workspace.py b/plane-src/apps/api/plane/license/api/views/workspace.py index 352e885..caa48ad 100644 --- a/plane-src/apps/api/plane/license/api/views/workspace.py +++ b/plane-src/apps/api/plane/license/api/views/workspace.py @@ -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() diff --git a/plane-src/apps/api/plane/space/utils/grouper.py b/plane-src/apps/api/plane/space/utils/grouper.py index e5f893b..6f1fe6e 100644 --- a/plane-src/apps/api/plane/space/utils/grouper.py +++ b/plane-src/apps/api/plane/space/utils/grouper.py @@ -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", diff --git a/plane-src/apps/api/plane/space/views/issue.py b/plane-src/apps/api/plane/space/views/issue.py index 9e21874..a431298 100644 --- a/plane-src/apps/api/plane/space/views/issue.py +++ b/plane-src/apps/api/plane/space/views/issue.py @@ -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) ), ), diff --git a/plane-src/apps/api/plane/utils/grouper.py b/plane-src/apps/api/plane/utils/grouper.py index c34a6ea..961f1ff 100644 --- a/plane-src/apps/api/plane/utils/grouper.py +++ b/plane-src/apps/api/plane/utils/grouper.py @@ -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)) diff --git a/plane-src/apps/web/core/components/issues/issue-layouts/kanban/internal-contour-card.tsx b/plane-src/apps/web/core/components/issues/issue-layouts/kanban/internal-contour-card.tsx index 62dc571..4c8c70a 100644 --- a/plane-src/apps/web/core/components/issues/issue-layouts/kanban/internal-contour-card.tsx +++ b/plane-src/apps/web/core/components/issues/issue-layouts/kanban/internal-contour-card.tsx @@ -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 =