From 9a7bff79506da81ff166477f5f4cb70ad49b75bd Mon Sep 17 00:00:00 2001 From: Nikhil <118773738+pablohashescobar@users.noreply.github.com> Date: Wed, 16 Oct 2024 20:14:03 +0530 Subject: [PATCH 1/2] chore: image delete restore auth endpoint (#1531) Co-authored-by: sriram veeraghanta --- apiserver/plane/app/views/asset/base.py | 7 +++++++ apiserver/plane/app/views/asset/v2.py | 2 ++ 2 files changed, 9 insertions(+) diff --git a/apiserver/plane/app/views/asset/base.py b/apiserver/plane/app/views/asset/base.py index 6de4a4ee7f..203b8d8dff 100644 --- a/apiserver/plane/app/views/asset/base.py +++ b/apiserver/plane/app/views/asset/base.py @@ -2,11 +2,13 @@ from rest_framework import status from rest_framework.response import Response from rest_framework.parsers import MultiPartParser, FormParser, JSONParser +from rest_framework_simplejwt.authentication import JWTAuthentication # Module imports from ..base import BaseAPIView, BaseViewSet from plane.db.models import FileAsset, Workspace from plane.app.serializers import FileAssetSerializer +from plane.authentication.session import BaseSessionAuthentication class FileAssetEndpoint(BaseAPIView): @@ -20,6 +22,8 @@ class FileAssetEndpoint(BaseAPIView): A viewset for viewing and editing task instances. """ + authentication_classes = [JWTAuthentication, BaseSessionAuthentication] + def get(self, request, workspace_id, asset_key): asset_key = str(workspace_id) + "/" + asset_key files = FileAsset.objects.filter(asset=asset_key) @@ -55,6 +59,9 @@ class FileAssetEndpoint(BaseAPIView): class FileAssetViewSet(BaseViewSet): + + authentication_classes = [JWTAuthentication, BaseSessionAuthentication] + def restore(self, request, workspace_id, asset_key): asset_key = str(workspace_id) + "/" + asset_key file_asset = FileAsset.objects.get(asset=asset_key) diff --git a/apiserver/plane/app/views/asset/v2.py b/apiserver/plane/app/views/asset/v2.py index 5d12815db1..7662e9b6bf 100644 --- a/apiserver/plane/app/views/asset/v2.py +++ b/apiserver/plane/app/views/asset/v2.py @@ -565,6 +565,8 @@ class StaticFileAssetEndpoint(BaseAPIView): class AssetRestoreEndpoint(BaseAPIView): """Endpoint to restore a deleted assets.""" + authentication_classes = [JWTAuthentication, BaseSessionAuthentication] + @allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE") def post(self, request, slug, asset_id): asset = FileAsset.all_objects.get(id=asset_id, workspace__slug=slug) From be7013baea32f02b483edccfd4068b1cb577e2f8 Mon Sep 17 00:00:00 2001 From: guru_sainath Date: Thu, 17 Oct 2024 15:46:49 +0530 Subject: [PATCH 2/2] [MOBIL-166] chore: update issue property activity for issue properties (#1533) * chore: updated issue property activity for issue properties * fix: issue activity error * fix: issue activity target_date error and added in activity for issue realation * fix: issue activity for issue relations * fix: jwt request user * fix: issue activity --- .../graphql/bgtasks/issue_activity_task.py | 83 +++++++++-- apiserver/plane/graphql/mutations/issue.py | 129 ++++++++++++++---- .../graphql/mutations/issues/relation.py | 50 +++++++ .../graphql/mutations/issues/sub_issue.py | 24 ++++ .../graphql/queries/issues/attachment.py | 3 - .../plane/graphql/utils/issue_activity.py | 56 ++++++++ apiserver/plane/graphql/views.py | 4 + 7 files changed, 301 insertions(+), 48 deletions(-) create mode 100644 apiserver/plane/graphql/utils/issue_activity.py diff --git a/apiserver/plane/graphql/bgtasks/issue_activity_task.py b/apiserver/plane/graphql/bgtasks/issue_activity_task.py index bc9f039156..9d567361d8 100644 --- a/apiserver/plane/graphql/bgtasks/issue_activity_task.py +++ b/apiserver/plane/graphql/bgtasks/issue_activity_task.py @@ -10,7 +10,7 @@ from django.core.serializers.json import DjangoJSONEncoder from django.utils import timezone from plane.app.serializers import IssueActivitySerializer -from .notification_task import notifications +from plane.bgtasks.notification_task import notifications # Module imports from plane.db.models import ( @@ -27,6 +27,7 @@ from plane.db.models import ( State, User, EstimatePoint, + IssueType, ) from plane.settings.redis import redis_instance from plane.utils.exception_logger import log_exception @@ -465,7 +466,7 @@ def track_estimate_points( IssueActivity( issue_id=issue_id, actor_id=actor_id, - verb="updated", + verb="removed" if new_estimate is None else "updated", old_identifier=( current_instance.get("estimate_point") if current_instance.get("estimate_point") is not None @@ -570,6 +571,50 @@ def track_closed_to( ) +def track_type( + requested_data, + current_instance, + issue_id, + project_id, + workspace_id, + actor_id, + issue_activities, + epoch, +): + new_type_id = requested_data.get("type_id") + old_type_id = current_instance.get("type_id") + + if new_type_id != old_type_id: + verb = "updated" if new_type_id else "deleted" + old_type_id = ( + IssueType.objects.filter(pk=old_type_id).first() + if old_type_id + else None + ) + new_type_id = ( + IssueType.objects.filter(pk=new_type_id).first() + if new_type_id + else None + ) + + issue_activities.append( + IssueActivity( + issue_id=issue_id, + actor_id=actor_id, + verb=verb, + old_value=old_type_id.name if old_type_id else None, + new_value=new_type_id.name if new_type_id else None, + field="type", + project_id=project_id, + workspace_id=workspace_id, + comment="", + old_identifier=old_type_id.id if old_type_id else None, + new_identifier=new_type_id.id if new_type_id else None, + epoch=epoch, + ) + ) + + def create_issue_activity( requested_data, current_instance, @@ -580,17 +625,19 @@ def create_issue_activity( issue_activities, epoch, ): - issue_activities.append( - IssueActivity( - issue_id=issue_id, - project_id=project_id, - workspace_id=workspace_id, - comment="created the issue", - verb="created", - actor_id=actor_id, - epoch=epoch, - ) + issue = Issue.objects.get(pk=issue_id) + issue_activity = IssueActivity.objects.create( + issue_id=issue_id, + project_id=project_id, + workspace_id=workspace_id, + comment="created the issue", + verb="created", + actor_id=actor_id, + epoch=epoch, ) + issue_activity.created_at = issue.created_at + issue_activity.actor_id = issue.created_by_id + issue_activity.save(update_fields=["created_at", "actor_id"]) requested_data = ( json.loads(requested_data) if requested_data is not None else None ) @@ -630,6 +677,7 @@ def update_issue_activity( "estimate_point": track_estimate_points, "archived_at": track_archive_at, "closed_to": track_closed_to, + "type_id": track_type, } requested_data = ( @@ -668,6 +716,7 @@ def delete_issue_activity( IssueActivity( project_id=project_id, workspace_id=workspace_id, + issue_id=issue_id, comment="deleted the issue", verb="deleted", actor_id=actor_id, @@ -875,7 +924,6 @@ def delete_cycle_issue_activity( cycle_name = requested_data.get("cycle_name", "") cycle = Cycle.objects.filter(pk=cycle_id).first() issues = requested_data.get("issues") - for issue in issues: current_issue = Issue.objects.filter(pk=issue).first() if issue: @@ -1389,6 +1437,7 @@ def create_issue_relation_activity( workspace_id=workspace_id, comment=f"added {requested_data.get('relation_type')} relation", old_identifier=related_issue, + epoch=epoch, ) ) issue = Issue.objects.get(pk=issue_id) @@ -1697,12 +1746,16 @@ def issue_activity( event=( "issue_comment" if activity.field == "comment" - else "inbox_issue" if inbox else "issue" + else "inbox_issue" + if inbox + else "issue" ), event_id=( activity.issue_comment_id if activity.field == "comment" - else inbox if inbox else activity.issue_id + else inbox + if inbox + else activity.issue_id ), verb=activity.verb, field=( diff --git a/apiserver/plane/graphql/mutations/issue.py b/apiserver/plane/graphql/mutations/issue.py index dc09435f1f..73c619c924 100644 --- a/apiserver/plane/graphql/mutations/issue.py +++ b/apiserver/plane/graphql/mutations/issue.py @@ -1,5 +1,9 @@ # Python imports from datetime import datetime +import json + +# Django imports +from django.utils import timezone # Strawberry imports import strawberry @@ -27,6 +31,10 @@ from plane.db.models import ( FileAsset, IssueSubscriber, ) +from plane.graphql.bgtasks.issue_activity_task import issue_activity +from plane.graphql.utils.issue_activity import ( + convert_issue_properties_to_activity_dict, +) @strawberry.type @@ -46,7 +54,7 @@ class IssueMutation: priority: str, labels: Optional[list[strawberry.ID]] = None, assignees: Optional[list[strawberry.ID]] = None, - descriptionHtml: Optional[str] = {}, + descriptionHtml: Optional[str] = None, parent: Optional[str] = None, estimatePoint: Optional[str] = None, startDate: Optional[datetime] = None, @@ -58,7 +66,9 @@ class IssueMutation: project_id=project, priority=priority, state_id=state, - description_html=descriptionHtml, + description_html=descriptionHtml + if descriptionHtml is not None + else "

", parent_id=parent, estimate_point_id=estimatePoint, start_date=startDate, @@ -98,20 +108,42 @@ class IssueMutation: batch_size=10, ) - # # Track the issue - # issue_activity.delay( - # type="issue.activity.created", - # requested_data=json.dumps( - # self.request.data, cls=DjangoJSONEncoder - # ), - # actor_id=str(info.context.user.id), - # issue_id=str(issue.id), - # project_id=str(project), - # current_instance=None, - # epoch=int(timezone.now().timestamp()), - # notification=True, - # origin=info.context.request.META.get("HTTP_ORIGIN"), - # ) + activity_payload = {} + if name is not None: + activity_payload["name"] = name + if descriptionHtml is not None: + activity_payload["description_html"] = descriptionHtml + else: + activity_payload["description_html"] = "

" + if priority is not None: + activity_payload["priority"] = priority + if state is not None: + activity_payload["state_id"] = state + if parent is not None: + activity_payload["parent_id"] = parent + if estimatePoint is not None: + activity_payload["estimate_point"] = estimatePoint + if startDate is not None: + activity_payload["start_date"] = startDate.strftime("%Y-%m-%d") + if targetDate is not None: + activity_payload["target_date"] = targetDate.strftime("%Y-%m-%d") + if labels is not None: + activity_payload["label_ids"] = labels + if assignees is not None: + activity_payload["assignee_ids"] = assignees + + # Track the issue + await sync_to_async(issue_activity.delay)( + type="issue.activity.created", + origin=info.context.request.META.get("HTTP_ORIGIN"), + epoch=int(timezone.now().timestamp()), + notification=True, + project_id=str(project), + issue_id=str(issue.id), + actor_id=str(info.context.user.id), + current_instance=None, + requested_data=json.dumps(activity_payload), + ) return issue @@ -139,28 +171,45 @@ class IssueMutation: ) -> IssuesType: issue = await sync_to_async(Issue.objects.get)(id=id) + # activity tacking data + current_issue_activity = ( + await convert_issue_properties_to_activity_dict(issue) + ) + activity_payload = {} + if name is not None: issue.name = name + activity_payload["name"] = name if priority is not None: issue.priority = priority + activity_payload["priority"] = priority if state is not None: issue.state_id = state + activity_payload["state_id"] = state if descriptionHtml is not None: issue.description_html = descriptionHtml + activity_payload["description_html"] = descriptionHtml if parent is not None: issue.parent_id = parent + activity_payload["parent_id"] = parent if estimatePoint is not None: issue.estimate_point_id = estimatePoint + activity_payload["estimate_point"] = estimatePoint if startDate is not None: issue.start_date = startDate + activity_payload["start_date"] = startDate.strftime("%Y-%m-%d") if targetDate is not None: issue.target_date = targetDate + activity_payload["target_date"] = targetDate.strftime("%Y-%m-%d") workspace = await sync_to_async(Workspace.objects.get)(slug=slug) - # Save the updated issue + + # updating the issue await sync_to_async(issue.save)() + # creating or updating the assignees if assignees is not None: + activity_payload["assignee_ids"] = assignees await sync_to_async( IssueAssignee.objects.filter(issue=issue).delete )() @@ -179,7 +228,9 @@ class IssueMutation: batch_size=10, ) + # creating or updating the labels if labels is not None: + activity_payload["label_ids"] = labels await sync_to_async( IssueLabel.objects.filter(issue=issue).delete )() @@ -199,19 +250,17 @@ class IssueMutation: ) # Track the issue - # issue_activity.delay( - # type="issue.activity.created", - # requested_data=json.dumps( - # self.request.data, cls=DjangoJSONEncoder - # ), - # actor_id=str(info.context.user.id), - # issue_id=str(issue.id), - # project_id=str(project), - # current_instance=None, - # epoch=int(timezone.now().timestamp()), - # notification=True, - # origin=info.context.request.META.get("HTTP_ORIGIN"), - # ) + issue_activity.delay( + type="issue.activity.updated", + origin=info.context.request.META.get("HTTP_ORIGIN"), + epoch=int(timezone.now().timestamp()), + notification=True, + project_id=str(project), + issue_id=str(issue.id), + actor_id=str(info.context.user.id), + current_instance=json.dumps(current_issue_activity), + requested_data=json.dumps(activity_payload), + ) return issue @@ -230,8 +279,28 @@ class IssueMutation: issue = await sync_to_async(Issue.issue_objects.get)( id=issue, project_id=project, workspace__slug=slug ) + + if issue.created_by_id != info.context.user.id: + raise Exception("You are not authorized to delete this issue") + + # activity tracking data + current_issue_activity = ( + await convert_issue_properties_to_activity_dict(issue) + ) await sync_to_async(issue.delete)() + # Track the issue + issue_activity.delay( + type="issue.activity.deleted", + requested_data=json.dumps({"issue_id": str(issue)}), + actor_id=str(info.context.user.id), + issue_id=str(issue), + project_id=str(project), + current_instance=current_issue_activity, + epoch=int(timezone.now().timestamp()), + origin=info.context.request.META.get("HTTP_ORIGIN"), + ) + return True diff --git a/apiserver/plane/graphql/mutations/issues/relation.py b/apiserver/plane/graphql/mutations/issues/relation.py index 510deac865..53027675a1 100644 --- a/apiserver/plane/graphql/mutations/issues/relation.py +++ b/apiserver/plane/graphql/mutations/issues/relation.py @@ -1,3 +1,9 @@ +# Python imports +import json + +# Django imports +from django.utils import timezone + # Strawberry imports import strawberry from strawberry.types import Info @@ -9,6 +15,10 @@ from asgiref.sync import sync_to_async # Module imports from plane.graphql.permissions.project import ProjectBasePermission from plane.db.models import Workspace, IssueRelation +from plane.graphql.bgtasks.issue_activity_task import issue_activity +from plane.graphql.utils.issue_activity import ( + convert_issue_relation_properties_to_activity_dict, +) @strawberry.type @@ -61,6 +71,21 @@ class IssueRelationMutation: ) )() + # Track the issue relation activity + issue_activity.delay( + type="issue_relation.activity.created", + requested_data=json.dumps( + {"issues": related_issue_ids, "relation_type": relation_type} + ), + actor_id=str(info.context.user.id), + issue_id=str(issue), + project_id=str(project), + current_instance=None, + epoch=int(timezone.now().timestamp()), + notification=True, + origin=info.context.request.META.get("HTTP_ORIGIN"), + ) + return True # removing issue relation @@ -94,4 +119,29 @@ class IssueRelationMutation: await sync_to_async(lambda: issue_relation.delete())() + # current issue relation + current_issue_relation_instance = ( + await convert_issue_relation_properties_to_activity_dict( + issue_relation + ) + ) + + # Track the issue relation activity + issue_activity.delay( + type="issue_relation.activity.created", + requested_data=json.dumps( + { + "related_issue": related_issue, + "relation_type": relation_type, + } + ), + actor_id=str(info.context.user.id), + issue_id=str(issue), + project_id=str(project), + current_instance=json.dumps(current_issue_relation_instance), + epoch=int(timezone.now().timestamp()), + notification=True, + origin=info.context.request.META.get("HTTP_ORIGIN"), + ) + return True diff --git a/apiserver/plane/graphql/mutations/issues/sub_issue.py b/apiserver/plane/graphql/mutations/issues/sub_issue.py index 9c6df50030..8e784d2a34 100644 --- a/apiserver/plane/graphql/mutations/issues/sub_issue.py +++ b/apiserver/plane/graphql/mutations/issues/sub_issue.py @@ -1,5 +1,10 @@ +# Python imports +import json from typing import Optional +# Django imports +from django.utils import timezone + # Strawberry imports import strawberry from strawberry.types import Info @@ -12,6 +17,7 @@ from asgiref.sync import sync_to_async from plane.graphql.permissions.project import ProjectMemberPermission from plane.db.models import Issue from plane.graphql.types.issue import IssuesType +from plane.graphql.bgtasks.issue_activity_task import issue_activity # create methods @@ -65,6 +71,24 @@ class SubIssueMutation: sub_issue.parent_id = parentIssueId await bulk_update_issues(sub_issues, ["parent"]) + + _ = [ + issue_activity.delay( + type="issue.activity.updated", + requested_data=json.dumps( + {"parent_id": str(parentIssueId)} + ), + actor_id=str(info.context.user.id), + issue_id=str(sub_issue_id), + project_id=str(project), + current_instance=json.dumps({"parent_id": None}), + epoch=int(timezone.now().timestamp()), + notification=True, + origin=info.context.request.META.get("HTTP_ORIGIN"), + ) + for sub_issue_id in subIssueIds + ] + return True except Exception: return False diff --git a/apiserver/plane/graphql/queries/issues/attachment.py b/apiserver/plane/graphql/queries/issues/attachment.py index 844101c628..8078593a09 100644 --- a/apiserver/plane/graphql/queries/issues/attachment.py +++ b/apiserver/plane/graphql/queries/issues/attachment.py @@ -26,9 +26,6 @@ class IssueAttachmentQuery: project: strawberry.ID, issue: strawberry.ID, ) -> list[IssueAttachmentType]: - print("---*___") - print("Attachment query") - print("---*___") issue_attachments = await sync_to_async(list)( FileAsset.objects.filter( workspace__slug=slug, diff --git a/apiserver/plane/graphql/utils/issue_activity.py b/apiserver/plane/graphql/utils/issue_activity.py new file mode 100644 index 0000000000..98114c5e83 --- /dev/null +++ b/apiserver/plane/graphql/utils/issue_activity.py @@ -0,0 +1,56 @@ +# Third-party imports +from asgiref.sync import sync_to_async + + +@sync_to_async +def get_issue_labels(issue): + return [ + str(label_id) + for label_id in list(issue.labels.values_list("id", flat=True)) + ] + + +@sync_to_async +def get_issue_assignees(issue): + return [ + str(assignee_id) + for assignee_id in list(issue.assignees.values_list("id", flat=True)) + ] + + +# issue properties to activity dict +async def convert_issue_properties_to_activity_dict(issue): + current_issue_activity_labels = await get_issue_labels(issue) + current_issue_activity_assignees = await get_issue_assignees(issue) + + current_issue_activity = { + "name": issue.name, + "description_html": issue.description_html, + "priority": issue.priority, + "state_id": str(issue.state_id), + "parent_id": str(issue.parent_id) + if issue.parent_id is not None + else None, + "estimate_point": str(issue.estimate_point) + if issue.estimate_point is not NotImplementedError + else None, + "start_date": issue.start_date.strftime("%Y-%m-%d") + if issue.start_date is not None + else None, + "target_date": issue.target_date.strftime("%Y-%m-%d") + if issue.target_date is not None + else None, + "label_ids": current_issue_activity_labels, + "assignee_ids": current_issue_activity_assignees, + } + + return current_issue_activity + + +# issue relation properties to activity dict +async def convert_issue_relation_properties_to_activity_dict(issue_relation): + return { + "issue_id": str(issue_relation.issue_id), + "related_issue_id": str(issue_relation.related_issue_id), + "relation_type": issue_relation.relation_type, + } diff --git a/apiserver/plane/graphql/views.py b/apiserver/plane/graphql/views.py index 5d101c5110..1c2f5dfc6f 100644 --- a/apiserver/plane/graphql/views.py +++ b/apiserver/plane/graphql/views.py @@ -39,20 +39,24 @@ class CustomGraphQLView(AsyncGraphQLView): bearer_token = auth_header.split()[1] if not bearer_token: context.user = None + request.user = None validated_token = await get_validated_token(bearer_token) if validated_token is None: context.user = None + request.user = None # Get the user from the validated token user = await get_jwt_user(validated_token) # Set the user in the context context.user = user + request.user = user else: context.user = None except (InvalidToken, TokenError) as e: context.user = None + request.user = None return context async def process_result(