Merge pull request #1539 from makeplane/mobile-dev

promote: mobile-dev to mobile-uat
This commit is contained in:
Nikhil
2024-10-17 15:48:44 +05:30
committed by GitHub
9 changed files with 310 additions and 48 deletions

View File

@@ -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)

View File

@@ -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)

View File

@@ -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=(

View File

@@ -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 "<p></p>",
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"] = "<p></p>"
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

View File

@@ -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

View File

@@ -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

View File

@@ -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,

View File

@@ -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,
}

View File

@@ -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(