From 008e0489687789a1c54f2e9c616ab2fbd627482e Mon Sep 17 00:00:00 2001 From: Sangeetha Date: Wed, 20 Aug 2025 18:54:32 +0530 Subject: [PATCH 1/3] [WEB-4430] fix: incorrect WI count while scrolling (#7596) * fix: wrong WI count while scrolling * chore: optimize issue queryset * fix: use separate query for total_count_queryset * fix: guest visibility constraint * fix: use separate query for total_count_queryset in external api * fix: use queryset.count() --- apps/api/plane/api/views/issue.py | 5 ++ apps/api/plane/app/views/issue/archive.py | 56 ++++++++++++++++------- apps/api/plane/app/views/issue/base.py | 48 ++++++++++++------- apps/api/plane/utils/paginator.py | 2 +- 4 files changed, 76 insertions(+), 35 deletions(-) diff --git a/apps/api/plane/api/views/issue.py b/apps/api/plane/api/views/issue.py index 94dc9e1d03..5a75503d70 100644 --- a/apps/api/plane/api/views/issue.py +++ b/apps/api/plane/api/views/issue.py @@ -331,6 +331,10 @@ class IssueListCreateAPIEndpoint(BaseAPIView): ) ) + total_issue_queryset = Issue.issue_objects.filter( + project_id=project_id, workspace__slug=slug + ) + # Priority Ordering if order_by_param == "priority" or order_by_param == "-priority": priority_order = ( @@ -390,6 +394,7 @@ class IssueListCreateAPIEndpoint(BaseAPIView): return self.paginate( request=request, queryset=(issue_queryset), + total_count_queryset=total_issue_queryset, on_results=lambda issues: IssueSerializer( issues, many=True, fields=self.fields, expand=self.expand ).data, diff --git a/apps/api/plane/app/views/issue/archive.py b/apps/api/plane/app/views/issue/archive.py index 118d1e7f92..122f4bdc82 100644 --- a/apps/api/plane/app/views/issue/archive.py +++ b/apps/api/plane/app/views/issue/archive.py @@ -3,7 +3,7 @@ import json # Django imports from django.core.serializers.json import DjangoJSONEncoder -from django.db.models import F, Func, OuterRef, Q, Prefetch, Exists, Subquery +from django.db.models import F, Func, OuterRef, Q, Prefetch, Exists, Subquery, Count from django.utils import timezone from django.utils.decorators import method_decorator from django.views.decorators.gzip import gzip_page @@ -69,25 +69,31 @@ class IssueArchiveViewSet(BaseViewSet): ) ) .annotate( - link_count=IssueLink.objects.filter(issue=OuterRef("id")) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate( - attachment_count=FileAsset.objects.filter( - issue_id=OuterRef("id"), - entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT, + link_count=Subquery( + IssueLink.objects.filter(issue=OuterRef("id")) + .values("issue") + .annotate(count=Count("id")) + .values("count") ) - .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") + attachment_count=Subquery( + FileAsset.objects.filter( + issue_id=OuterRef("id"), + entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT, + ) + .values("issue_id") + .annotate(count=Count("id")) + .values("count") + ) + ) + .annotate( + sub_issues_count=Subquery( + Issue.issue_objects.filter(parent=OuterRef("id")) + .values("parent") + .annotate(count=Count("id")) + .values("count") + ) ) ) @@ -101,6 +107,19 @@ class IssueArchiveViewSet(BaseViewSet): issue_queryset = self.get_queryset().filter(**filters) + total_issue_queryset = Issue.objects.filter( + deleted_at__isnull=True, + archived_at__isnull=False, + project_id=project_id, + workspace__slug=slug, + ).filter(**filters) + + total_issue_queryset = ( + total_issue_queryset + if show_sub_issues == "true" + else total_issue_queryset.filter(parent__isnull=True) + ) + issue_queryset = ( issue_queryset if show_sub_issues == "true" @@ -136,6 +155,7 @@ class IssueArchiveViewSet(BaseViewSet): request=request, order_by=order_by_param, queryset=issue_queryset, + total_count_queryset=total_issue_queryset, on_results=lambda issues: issue_on_results( group_by=group_by, issues=issues, sub_group_by=sub_group_by ), @@ -170,6 +190,7 @@ class IssueArchiveViewSet(BaseViewSet): request=request, order_by=order_by_param, queryset=issue_queryset, + total_count_queryset=total_issue_queryset, on_results=lambda issues: issue_on_results( group_by=group_by, issues=issues, sub_group_by=sub_group_by ), @@ -196,6 +217,7 @@ class IssueArchiveViewSet(BaseViewSet): order_by=order_by_param, request=request, queryset=issue_queryset, + total_count_queryset=total_issue_queryset, on_results=lambda issues: issue_on_results( group_by=group_by, issues=issues, sub_group_by=sub_group_by ), diff --git a/apps/api/plane/app/views/issue/base.py b/apps/api/plane/app/views/issue/base.py index 6944ed98f8..21e5eaf709 100644 --- a/apps/api/plane/app/views/issue/base.py +++ b/apps/api/plane/app/views/issue/base.py @@ -213,27 +213,33 @@ class IssueViewSet(BaseViewSet): ) ) .annotate( - link_count=IssueLink.objects.filter(issue=OuterRef("id")) - .order_by() - .annotate(count=Func(F("id"), function="Count")) - .values("count") - ) - .annotate( - attachment_count=FileAsset.objects.filter( - issue_id=OuterRef("id"), - entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT, + link_count=Subquery( + IssueLink.objects.filter(issue=OuterRef("id")) + .values("issue") + .annotate(count=Count("id")) + .values("count") ) - .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") + attachment_count=Subquery( + FileAsset.objects.filter( + issue_id=OuterRef("id"), + entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT, + ) + .values("issue_id") + .annotate(count=Count("id")) + .values("count") + ) ) - ).distinct() + .annotate( + sub_issues_count=Subquery( + Issue.issue_objects.filter(parent=OuterRef("id")) + .values("parent") + .annotate(count=Count("id")) + .values("count") + ) + ) + ) @method_decorator(gzip_page) @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST]) @@ -249,6 +255,10 @@ class IssueViewSet(BaseViewSet): issue_queryset = self.get_queryset().filter(**filters, **extra_filters) # Custom ordering for priority and state + total_issue_queryset = Issue.issue_objects.filter( + project_id=project_id, workspace__slug=slug + ).filter(**filters, **extra_filters) + # Issue queryset issue_queryset, order_by_param = order_issue_queryset( issue_queryset=issue_queryset, order_by_param=order_by_param @@ -281,6 +291,7 @@ class IssueViewSet(BaseViewSet): and not project.guest_view_all_features ): issue_queryset = issue_queryset.filter(created_by=request.user) + total_issue_queryset = total_issue_queryset.filter(created_by=request.user) if group_by: if sub_group_by: @@ -296,6 +307,7 @@ class IssueViewSet(BaseViewSet): request=request, order_by=order_by_param, queryset=issue_queryset, + total_count_queryset=total_issue_queryset, on_results=lambda issues: issue_on_results( group_by=group_by, issues=issues, sub_group_by=sub_group_by ), @@ -329,6 +341,7 @@ class IssueViewSet(BaseViewSet): request=request, order_by=order_by_param, queryset=issue_queryset, + total_count_queryset=total_issue_queryset, on_results=lambda issues: issue_on_results( group_by=group_by, issues=issues, sub_group_by=sub_group_by ), @@ -354,6 +367,7 @@ class IssueViewSet(BaseViewSet): order_by=order_by_param, request=request, queryset=issue_queryset, + total_count_queryset=total_issue_queryset, on_results=lambda issues: issue_on_results( group_by=group_by, issues=issues, sub_group_by=sub_group_by ), diff --git a/apps/api/plane/utils/paginator.py b/apps/api/plane/utils/paginator.py index ce9c65f644..0d065e2531 100644 --- a/apps/api/plane/utils/paginator.py +++ b/apps/api/plane/utils/paginator.py @@ -160,7 +160,7 @@ class OffsetPaginator: total_count = ( self.total_count_queryset.count() if self.total_count_queryset - else results.count() + else queryset.count() ) # Check if there are more results available after the current page From 174ebfad56b2abc8b448f3ebd29914d67c229c85 Mon Sep 17 00:00:00 2001 From: Akshita Goyal <36129505+gakshita@users.noreply.github.com> Date: Wed, 20 Aug 2025 18:55:22 +0530 Subject: [PATCH 2/3] [WEB-4637] fix: scrolling issue fixed #7600 --- apps/web/core/components/home/root.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/web/core/components/home/root.tsx b/apps/web/core/components/home/root.tsx index 89c55cb91e..5a0e6188f8 100644 --- a/apps/web/core/components/home/root.tsx +++ b/apps/web/core/components/home/root.tsx @@ -60,11 +60,11 @@ export const WorkspaceHomeView = observer(() => { )} <> - - {currentUser && } - + +
+ {currentUser && } + +
From a59ebadd348ba3d154a063a498b5ea5ef4ab7452 Mon Sep 17 00:00:00 2001 From: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com> Date: Wed, 20 Aug 2025 18:56:15 +0530 Subject: [PATCH 3/3] [WEB-4712] chore: work item attachment patch endpoint (#7595) --- apps/api/plane/api/urls/issue.py | 4 +- apps/api/plane/api/views/issue.py | 82 +++++++++++++++++++++++++++++-- 2 files changed, 82 insertions(+), 4 deletions(-) diff --git a/apps/api/plane/api/urls/issue.py b/apps/api/plane/api/urls/issue.py index c8d1ea4afe..df64684de2 100644 --- a/apps/api/plane/api/urls/issue.py +++ b/apps/api/plane/api/urls/issue.py @@ -89,7 +89,9 @@ urlpatterns = [ ), path( "workspaces//projects//issues//issue-attachments//", - IssueAttachmentDetailAPIEndpoint.as_view(http_method_names=["get", "delete"]), + IssueAttachmentDetailAPIEndpoint.as_view( + http_method_names=["get", "patch", "delete"] + ), name="issue-attachment", ), ] diff --git a/apps/api/plane/api/views/issue.py b/apps/api/plane/api/views/issue.py index 5a75503d70..489fa4a088 100644 --- a/apps/api/plane/api/views/issue.py +++ b/apps/api/plane/api/views/issue.py @@ -75,7 +75,7 @@ from plane.bgtasks.storage_metadata_task import get_asset_object_metadata from .base import BaseAPIView from plane.utils.host import base_host from plane.bgtasks.webhook_task import model_activity - +from plane.app.permissions import ROLE from plane.utils.openapi import ( work_item_docs, label_docs, @@ -145,6 +145,22 @@ from plane.utils.openapi import ( ) from plane.bgtasks.work_item_link_task import crawl_work_item_link_title +def user_has_issue_permission( + user_id, project_id, issue=None, allowed_roles=None, allow_creator=True +): + if allow_creator and issue is not None and user_id == issue.created_by_id: + return True + + qs = ProjectMember.objects.filter( + project_id=project_id, + member_id=user_id, + is_active=True, + ) + if allowed_roles is not None: + qs = qs.filter(role__in=allowed_roles) + + return qs.exists() + class WorkspaceIssueAPIEndpoint(BaseAPIView): """ @@ -1787,7 +1803,6 @@ class IssueAttachmentListCreateAPIEndpoint(BaseAPIView): serializer_class = IssueAttachmentSerializer model = FileAsset - permission_classes = [ProjectEntityPermission] use_read_replica = True @issue_attachment_docs( @@ -1870,6 +1885,22 @@ class IssueAttachmentListCreateAPIEndpoint(BaseAPIView): Generate presigned URL for uploading file attachments to a work item. Validates file type and size before creating the attachment record. """ + issue = Issue.objects.get( + pk=issue_id, workspace__slug=slug, project_id=project_id + ) + # if the user is creator or admin,member then allow the upload + if not user_has_issue_permission( + request.user.id, + project_id=project_id, + issue=issue, + allowed_roles=[ROLE.ADMIN.value, ROLE.MEMBER.value], + allow_creator=True, + ): + return Response( + {"error": "You are not allowed to upload this attachment"}, + status=status.HTTP_403_FORBIDDEN, + ) + name = request.data.get("name") type = request.data.get("type", False) size = request.data.get("size") @@ -1994,7 +2025,6 @@ class IssueAttachmentDetailAPIEndpoint(BaseAPIView): """Issue Attachment Detail Endpoint""" serializer_class = IssueAttachmentSerializer - permission_classes = [ProjectEntityPermission] model = FileAsset use_read_replica = True @@ -2017,6 +2047,22 @@ class IssueAttachmentDetailAPIEndpoint(BaseAPIView): Soft delete an attachment from a work item by marking it as deleted. Records deletion activity and triggers metadata cleanup. """ + issue = Issue.objects.get( + pk=issue_id, workspace__slug=slug, project_id=project_id + ) + # if the request user is creator or admin then delete the attachment + if not user_has_issue_permission( + request.user, + project_id=project_id, + issue=issue, + allowed_roles=[ROLE.ADMIN.value], + allow_creator=True, + ): + return Response( + {"error": "You are not allowed to delete this attachment"}, + status=status.HTTP_403_FORBIDDEN, + ) + issue_attachment = FileAsset.objects.get( pk=pk, workspace__slug=slug, project_id=project_id ) @@ -2079,6 +2125,19 @@ class IssueAttachmentDetailAPIEndpoint(BaseAPIView): Retrieve details of a specific attachment. """ + # if the user is part of the project then allow the download + if not user_has_issue_permission( + request.user, + project_id=project_id, + issue=None, + allowed_roles=None, + allow_creator=False, + ): + return Response( + {"error": "You are not allowed to download this attachment"}, + status=status.HTTP_403_FORBIDDEN, + ) + # Get the asset asset = FileAsset.objects.get( id=pk, workspace__slug=slug, project_id=project_id @@ -2133,6 +2192,23 @@ class IssueAttachmentDetailAPIEndpoint(BaseAPIView): Mark an attachment as uploaded after successful file transfer to storage. Triggers activity logging and metadata extraction. """ + + issue = Issue.objects.get( + pk=issue_id, workspace__slug=slug, project_id=project_id + ) + # if the user is creator or admin then allow the upload + if not user_has_issue_permission( + request.user, + project_id=project_id, + issue=issue, + allowed_roles=[ROLE.ADMIN.value, ROLE.MEMBER.value], + allow_creator=True, + ): + return Response( + {"error": "You are not allowed to upload this attachment"}, + status=status.HTTP_403_FORBIDDEN, + ) + issue_attachment = FileAsset.objects.get( pk=pk, workspace__slug=slug, project_id=project_id )