From 650328c6f28bd275f3c3ea8317f57696a826809a Mon Sep 17 00:00:00 2001 From: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com> Date: Tue, 20 Aug 2024 19:40:48 +0530 Subject: [PATCH] [WEB-1986] fix: remove the user favourites when archived a particular entity (#5387) * chore: pages custom error codes * fix: view role permission --- apiserver/plane/api/views/cycle.py | 6 + apiserver/plane/api/views/module.py | 6 + apiserver/plane/api/views/project.py | 4 + apiserver/plane/app/urls/issue.py | 8 +- apiserver/plane/app/views/__init__.py | 3 - apiserver/plane/app/views/cycle/archive.py | 6 + apiserver/plane/app/views/issue/archive.py | 5 +- .../plane/app/views/issue/bulk_operations.py | 293 ------------------ apiserver/plane/app/views/module/archive.py | 6 + apiserver/plane/app/views/page/base.py | 28 +- apiserver/plane/app/views/project/base.py | 4 + apiserver/plane/app/views/view/base.py | 15 +- apiserver/plane/utils/error_codes.py | 10 + 13 files changed, 71 insertions(+), 323 deletions(-) delete mode 100644 apiserver/plane/app/views/issue/bulk_operations.py create mode 100644 apiserver/plane/utils/error_codes.py diff --git a/apiserver/plane/api/views/cycle.py b/apiserver/plane/api/views/cycle.py index a0c11d11ef..3814466322 100644 --- a/apiserver/plane/api/views/cycle.py +++ b/apiserver/plane/api/views/cycle.py @@ -544,6 +544,12 @@ class CycleArchiveUnarchiveAPIEndpoint(BaseAPIView): ) cycle.archived_at = timezone.now() cycle.save() + UserFavorite.objects.filter( + entity_type="cycle", + entity_identifier=cycle_id, + project_id=project_id, + workspace__slug=slug, + ).delete() return Response(status=status.HTTP_204_NO_CONTENT) def delete(self, request, slug, project_id, cycle_id): diff --git a/apiserver/plane/api/views/module.py b/apiserver/plane/api/views/module.py index f197e2eaa0..8c374baf6b 100644 --- a/apiserver/plane/api/views/module.py +++ b/apiserver/plane/api/views/module.py @@ -634,6 +634,12 @@ class ModuleArchiveUnarchiveAPIEndpoint(BaseAPIView): ) module.archived_at = timezone.now() module.save() + UserFavorite.objects.filter( + entity_type="module", + entity_identifier=pk, + project_id=project_id, + workspace__slug=slug, + ).delete() return Response(status=status.HTTP_204_NO_CONTENT) def delete(self, request, slug, project_id, pk): diff --git a/apiserver/plane/api/views/project.py b/apiserver/plane/api/views/project.py index 2f8dddd6d0..0052c9fe62 100644 --- a/apiserver/plane/api/views/project.py +++ b/apiserver/plane/api/views/project.py @@ -377,6 +377,10 @@ class ProjectArchiveUnarchiveAPIEndpoint(BaseAPIView): project = Project.objects.get(pk=project_id, workspace__slug=slug) project.archived_at = timezone.now() project.save() + UserFavorite.objects.filter( + workspace__slug=slug, + project=project_id, + ).delete() return Response(status=status.HTTP_204_NO_CONTENT) def delete(self, request, slug, project_id): diff --git a/apiserver/plane/app/urls/issue.py b/apiserver/plane/app/urls/issue.py index aa6a8e2f06..4ad7f61182 100644 --- a/apiserver/plane/app/urls/issue.py +++ b/apiserver/plane/app/urls/issue.py @@ -19,7 +19,6 @@ from plane.app.views import ( IssueUserDisplayPropertyEndpoint, IssueViewSet, LabelViewSet, - BulkIssueOperationsEndpoint, BulkArchiveIssuesEndpoint, ) @@ -304,10 +303,5 @@ urlpatterns = [ } ), name="project-issue-draft", - ), - path( - "workspaces//projects//bulk-operation-issues/", - BulkIssueOperationsEndpoint.as_view(), - name="bulk-operations-issues", - ), + ) ] diff --git a/apiserver/plane/app/views/__init__.py b/apiserver/plane/app/views/__init__.py index 9d8929fda5..5568542f70 100644 --- a/apiserver/plane/app/views/__init__.py +++ b/apiserver/plane/app/views/__init__.py @@ -156,9 +156,6 @@ from .issue.subscriber import ( IssueSubscriberViewSet, ) - -from .issue.bulk_operations import BulkIssueOperationsEndpoint - from .module.base import ( ModuleViewSet, ModuleLinkViewSet, diff --git a/apiserver/plane/app/views/cycle/archive.py b/apiserver/plane/app/views/cycle/archive.py index 5f7f143473..22f21f4bf3 100644 --- a/apiserver/plane/app/views/cycle/archive.py +++ b/apiserver/plane/app/views/cycle/archive.py @@ -607,6 +607,12 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView): cycle.archived_at = timezone.now() cycle.save() + UserFavorite.objects.filter( + entity_type="cycle", + entity_identifier=cycle_id, + project_id=project_id, + workspace__slug=slug, + ).delete() return Response( {"archived_at": str(cycle.archived_at)}, status=status.HTTP_200_OK, diff --git a/apiserver/plane/app/views/issue/archive.py b/apiserver/plane/app/views/issue/archive.py index 53dec68981..811e6f8f92 100644 --- a/apiserver/plane/app/views/issue/archive.py +++ b/apiserver/plane/app/views/issue/archive.py @@ -47,6 +47,7 @@ from plane.utils.paginator import ( SubGroupedOffsetPaginator, ) from plane.app.permissions import allow_permission, ROLE +from plane.utils.error_codes import ERROR_CODES # Module imports from .. import BaseViewSet, BaseAPIView @@ -345,7 +346,9 @@ class BulkArchiveIssuesEndpoint(BaseAPIView): if issue.state.group not in ["completed", "cancelled"]: return Response( { - "error_code": 4091, + "error_code": ERROR_CODES[ + "INVALID_ARCHIVE_STATE_GROUP" + ], "error_message": "INVALID_ARCHIVE_STATE_GROUP", }, status=status.HTTP_400_BAD_REQUEST, diff --git a/apiserver/plane/app/views/issue/bulk_operations.py b/apiserver/plane/app/views/issue/bulk_operations.py deleted file mode 100644 index 1965b4e31e..0000000000 --- a/apiserver/plane/app/views/issue/bulk_operations.py +++ /dev/null @@ -1,293 +0,0 @@ -# Python imports -import json -from datetime import datetime - -# Django imports -from django.utils import timezone - -# Third Party imports -from rest_framework.response import Response -from rest_framework import status - -# Module imports -from .. import BaseAPIView -from plane.app.permissions import ( - ProjectEntityPermission, -) -from plane.db.models import ( - Project, - Issue, - IssueLabel, - IssueAssignee, -) -from plane.bgtasks.issue_activities_task import issue_activity - - -class BulkIssueOperationsEndpoint(BaseAPIView): - permission_classes = [ - ProjectEntityPermission, - ] - - def post(self, request, slug, project_id): - issue_ids = request.data.get("issue_ids", []) - if not len(issue_ids): - return Response( - {"error": "Issue IDs are required"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - # Get all the issues - issues = ( - Issue.objects.filter( - workspace__slug=slug, project_id=project_id, pk__in=issue_ids - ) - .select_related("state") - .prefetch_related("labels", "assignees") - ) - # Current epoch - epoch = int(timezone.now().timestamp()) - - # Project details - project = Project.objects.get(workspace__slug=slug, pk=project_id) - workspace_id = project.workspace_id - - # Initialize arrays - bulk_update_issues = [] - bulk_issue_activities = [] - bulk_update_issue_labels = [] - bulk_update_issue_assignees = [] - - properties = request.data.get("properties", {}) - - if properties.get("start_date", False) and properties.get( - "target_date", False - ): - if ( - datetime.strptime( - properties.get("start_date"), "%Y-%m-%d" - ).date() - > datetime.strptime( - properties.get("target_date"), "%Y-%m-%d" - ).date() - ): - return Response( - { - "error_code": 4100, - "error_message": "INVALID_ISSUE_DATES", - }, - status=status.HTTP_400_BAD_REQUEST, - ) - - for issue in issues: - # Priority - if properties.get("priority", False): - bulk_issue_activities.append( - { - "type": "issue.activity.updated", - "requested_data": json.dumps( - {"priority": properties.get("priority")} - ), - "current_instance": json.dumps( - {"priority": (issue.priority)} - ), - "issue_id": str(issue.id), - "actor_id": str(request.user.id), - "project_id": str(project_id), - "epoch": epoch, - } - ) - issue.priority = properties.get("priority") - - # State - if properties.get("state_id", False): - bulk_issue_activities.append( - { - "type": "issue.activity.updated", - "requested_data": json.dumps( - {"state": properties.get("state")} - ), - "current_instance": json.dumps( - {"state": str(issue.state_id)} - ), - "issue_id": str(issue.id), - "actor_id": str(request.user.id), - "project_id": str(project_id), - "epoch": epoch, - } - ) - issue.state_id = properties.get("state_id") - - # Start date - if properties.get("start_date", False): - if ( - issue.target_date - and not properties.get("target_date", False) - and issue.target_date - <= datetime.strptime( - properties.get("start_date"), "%Y-%m-%d" - ).date() - ): - return Response( - { - "error_code": 4101, - "error_message": "INVALID_ISSUE_START_DATE", - }, - status=status.HTTP_400_BAD_REQUEST, - ) - bulk_issue_activities.append( - { - "type": "issue.activity.updated", - "requested_data": json.dumps( - {"start_date": properties.get("start_date")} - ), - "current_instance": json.dumps( - {"start_date": str(issue.start_date)} - ), - "issue_id": str(issue.id), - "actor_id": str(request.user.id), - "project_id": str(project_id), - "epoch": epoch, - } - ) - issue.start_date = properties.get("start_date") - - # Target date - if properties.get("target_date", False): - if ( - issue.start_date - and not properties.get("start_date", False) - and issue.start_date - >= datetime.strptime( - properties.get("target_date"), "%Y-%m-%d" - ).date() - ): - return Response( - { - "error_code": 4102, - "error_message": "INVALID_ISSUE_TARGET_DATE", - }, - status=status.HTTP_400_BAD_REQUEST, - ) - bulk_issue_activities.append( - { - "type": "issue.activity.updated", - "requested_data": json.dumps( - {"target_date": properties.get("target_date")} - ), - "current_instance": json.dumps( - {"target_date": str(issue.target_date)} - ), - "issue_id": str(issue.id), - "actor_id": str(request.user.id), - "project_id": str(project_id), - "epoch": epoch, - } - ) - issue.target_date = properties.get("target_date") - - bulk_update_issues.append(issue) - - # Labels - if properties.get("label_ids", []): - for label_id in properties.get("label_ids", []): - bulk_update_issue_labels.append( - IssueLabel( - issue=issue, - label_id=label_id, - created_by=request.user, - project_id=project_id, - workspace_id=workspace_id, - ) - ) - bulk_issue_activities.append( - { - "type": "issue.activity.updated", - "requested_data": json.dumps( - {"label_ids": properties.get("label_ids", [])} - ), - "current_instance": json.dumps( - { - "label_ids": [ - str(label.id) - for label in issue.labels.all() - ] - } - ), - "issue_id": str(issue.id), - "actor_id": str(request.user.id), - "project_id": str(project_id), - "epoch": epoch, - } - ) - - # Assignees - if properties.get("assignee_ids", []): - for assignee_id in properties.get( - "assignee_ids", issue.assignees - ): - bulk_update_issue_assignees.append( - IssueAssignee( - issue=issue, - assignee_id=assignee_id, - created_by=request.user, - project_id=project_id, - workspace_id=workspace_id, - ) - ) - bulk_issue_activities.append( - { - "type": "issue.activity.updated", - "requested_data": json.dumps( - { - "assignee_ids": properties.get( - "assignee_ids", [] - ) - } - ), - "current_instance": json.dumps( - { - "assignee_ids": [ - str(assignee.id) - for assignee in issue.assignees.all() - ] - } - ), - "issue_id": str(issue.id), - "actor_id": str(request.user.id), - "project_id": str(project_id), - "epoch": epoch, - } - ) - - # Bulk update all the objects - Issue.objects.bulk_update( - bulk_update_issues, - [ - "priority", - "start_date", - "target_date", - "state", - ], - batch_size=100, - ) - - # Create new labels - IssueLabel.objects.bulk_create( - bulk_update_issue_labels, - ignore_conflicts=True, - batch_size=100, - ) - - # Create new assignees - IssueAssignee.objects.bulk_create( - bulk_update_issue_assignees, - ignore_conflicts=True, - batch_size=100, - ) - # update the issue activity - [ - issue_activity.delay(**activity) - for activity in bulk_issue_activities - ] - - return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/app/views/module/archive.py b/apiserver/plane/app/views/module/archive.py index 243c680cad..b38d83487c 100644 --- a/apiserver/plane/app/views/module/archive.py +++ b/apiserver/plane/app/views/module/archive.py @@ -575,6 +575,12 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView): ) module.archived_at = timezone.now() module.save() + UserFavorite.objects.filter( + entity_type="module", + entity_identifier=module_id, + project_id=project_id, + workspace__slug=slug, + ).delete() return Response( {"archived_at": str(module.archived_at)}, status=status.HTTP_200_OK, diff --git a/apiserver/plane/app/views/page/base.py b/apiserver/plane/app/views/page/base.py index fdd1f2cca2..01fa6649c2 100644 --- a/apiserver/plane/app/views/page/base.py +++ b/apiserver/plane/app/views/page/base.py @@ -33,7 +33,7 @@ from plane.db.models import ( ProjectMember, ProjectPage, ) - +from plane.utils.error_codes import ERROR_CODES # Module imports from ..base import BaseAPIView, BaseViewSet @@ -305,6 +305,13 @@ class PageViewSet(BaseViewSet): status=status.HTTP_400_BAD_REQUEST, ) + UserFavorite.objects.filter( + entity_type="page", + entity_identifier=pk, + project_id=project_id, + workspace__slug=slug, + ).delete() + unarchive_archive_page_and_descendants(pk, datetime.now()) return Response( @@ -479,6 +486,11 @@ class PagesDescriptionViewSet(BaseViewSet): .filter(Q(owned_by=self.request.user) | Q(access=0)) .first() ) + if page is None: + return Response( + {"error": "Page not found"}, + status=404, + ) binary_data = page.description_binary def stream_data(): @@ -513,14 +525,20 @@ class PagesDescriptionViewSet(BaseViewSet): if page.is_locked: return Response( - {"error": "Page is locked"}, - status=471, + { + "error_code": ERROR_CODES["PAGE_LOCKED"], + "error_message": "PAGE_LOCKED", + }, + status=status.HTTP_400_BAD_REQUEST, ) if page.archived_at: return Response( - {"error": "Page is archived"}, - status=472, + { + "error_code": ERROR_CODES["PAGE_ARCHIVED"], + "error_message": "PAGE_ARCHIVED", + }, + status=status.HTTP_400_BAD_REQUEST, ) # Serialize the existing instance diff --git a/apiserver/plane/app/views/project/base.py b/apiserver/plane/app/views/project/base.py index f049775897..ebc0e83fd8 100644 --- a/apiserver/plane/app/views/project/base.py +++ b/apiserver/plane/app/views/project/base.py @@ -493,6 +493,10 @@ class ProjectArchiveUnarchiveEndpoint(BaseAPIView): project = Project.objects.get(pk=project_id, workspace__slug=slug) project.archived_at = timezone.now() project.save() + UserFavorite.objects.filter( + workspace__slug=slug, + project=project_id, + ).delete() return Response( {"archived_at": str(project.archived_at)}, status=status.HTTP_200_OK, diff --git a/apiserver/plane/app/views/view/base.py b/apiserver/plane/app/views/view/base.py index c16f7ef5d8..4a571ef257 100644 --- a/apiserver/plane/app/views/view/base.py +++ b/apiserver/plane/app/views/view/base.py @@ -149,7 +149,7 @@ class WorkspaceViewViewSet(BaseViewSet): ) @allow_permission( - allowed_roles=[ROLE.ADMIN], + allowed_roles=[], level="WORKSPACE", creator=True, model=IssueView, @@ -159,19 +159,6 @@ class WorkspaceViewViewSet(BaseViewSet): pk=pk, workspace__slug=slug, ) - if not ( - WorkspaceMember.objects.filter( - workspace__slug=slug, - member=request.user, - role=20, - is_active=True, - ).exists() - and workspace_view.owned_by_id != request.user.id - ): - return Response( - {"error": "You do not have permission to delete this view"}, - status=status.HTTP_403_FORBIDDEN, - ) workspace_member = WorkspaceMember.objects.filter( workspace__slug=slug, diff --git a/apiserver/plane/utils/error_codes.py b/apiserver/plane/utils/error_codes.py new file mode 100644 index 0000000000..15d38f6bf9 --- /dev/null +++ b/apiserver/plane/utils/error_codes.py @@ -0,0 +1,10 @@ +ERROR_CODES = { + # issues + "INVALID_ARCHIVE_STATE_GROUP": 4091, + "INVALID_ISSUE_DATES": 4100, + "INVALID_ISSUE_START_DATE": 4101, + "INVALID_ISSUE_TARGET_DATE": 4102, + # pages + "PAGE_LOCKED": 4701, + "PAGE_ARCHIVED": 4702, +}