feat: add issue attachment external endpoint (#6307)

This commit is contained in:
Nikhil
2025-01-03 14:29:40 +05:30
committed by GitHub
parent d6bcd8dd15
commit 3fd2550d69

View File

@@ -1,9 +1,10 @@
# Python imports
import json
from django.core.serializers.json import DjangoJSONEncoder
import uuid
# Django imports
from django.core.serializers.json import DjangoJSONEncoder
from django.http import HttpResponseRedirect
from django.db import IntegrityError
from django.db.models import (
Case,
@@ -19,11 +20,11 @@ from django.db.models import (
Subquery,
)
from django.utils import timezone
from django.conf import settings
# Third party imports
from rest_framework import status
from rest_framework.response import Response
from rest_framework.parsers import MultiPartParser, FormParser
# Module imports
from plane.api.serializers import (
@@ -50,8 +51,10 @@ from plane.db.models import (
Project,
ProjectMember,
CycleIssue,
Workspace,
)
from plane.settings.storage import S3Storage
from plane.bgtasks.storage_metadata_task import get_asset_object_metadata
from .base import BaseAPIView
@@ -940,37 +943,162 @@ class IssueAttachmentEndpoint(BaseAPIView):
serializer_class = IssueAttachmentSerializer
permission_classes = [ProjectEntityPermission]
model = FileAsset
parser_classes = (MultiPartParser, FormParser)
def post(self, request, slug, project_id, issue_id):
serializer = IssueAttachmentSerializer(data=request.data)
name = request.data.get("name")
type = request.data.get("type", False)
size = request.data.get("size")
external_id = request.data.get("external_id")
external_source = request.data.get("external_source")
# Check if the request is valid
if not name or not size:
return Response(
{"error": "Invalid request.", "status": False},
status=status.HTTP_400_BAD_REQUEST,
)
size_limit = min(size, settings.FILE_SIZE_LIMIT)
if not type or type not in settings.ATTACHMENT_MIME_TYPES:
return Response(
{"error": "Invalid file type.", "status": False},
status=status.HTTP_400_BAD_REQUEST,
)
# Get the workspace
workspace = Workspace.objects.get(slug=slug)
# asset key
asset_key = f"{workspace.id}/{uuid.uuid4().hex}-{name}"
if (
request.data.get("external_id")
and request.data.get("external_source")
and FileAsset.objects.filter(
project_id=project_id,
workspace__slug=slug,
issue_id=issue_id,
external_source=request.data.get("external_source"),
external_id=request.data.get("external_id"),
issue_id=issue_id,
entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
).exists()
):
issue_attachment = FileAsset.objects.filter(
workspace__slug=slug,
asset = FileAsset.objects.filter(
project_id=project_id,
external_id=request.data.get("external_id"),
workspace__slug=slug,
external_source=request.data.get("external_source"),
external_id=request.data.get("external_id"),
issue_id=issue_id,
entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
).first()
return Response(
{
"error": "Issue attachment with the same external id and external source already exists",
"id": str(issue_attachment.id),
"error": "Issue with the same external id and external source already exists",
"id": str(asset.id),
},
status=status.HTTP_409_CONFLICT,
)
if serializer.is_valid():
serializer.save(project_id=project_id, issue_id=issue_id)
# Create a File Asset
asset = FileAsset.objects.create(
attributes={"name": name, "type": type, "size": size_limit},
asset=asset_key,
size=size_limit,
workspace_id=workspace.id,
created_by=request.user,
issue_id=issue_id,
project_id=project_id,
entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
external_id=external_id,
external_source=external_source,
)
# Get the presigned URL
storage = S3Storage(request=request)
# Generate a presigned URL to share an S3 object
presigned_url = storage.generate_presigned_post(
object_name=asset_key, file_type=type, file_size=size_limit
)
# Return the presigned URL
return Response(
{
"upload_data": presigned_url,
"asset_id": str(asset.id),
"attachment": IssueAttachmentSerializer(asset).data,
"asset_url": asset.asset_url,
},
status=status.HTTP_200_OK,
)
def delete(self, request, slug, project_id, issue_id, pk):
issue_attachment = FileAsset.objects.get(
pk=pk, workspace__slug=slug, project_id=project_id
)
issue_attachment.is_deleted = True
issue_attachment.deleted_at = timezone.now()
issue_attachment.save()
issue_activity.delay(
type="attachment.activity.deleted",
requested_data=None,
actor_id=str(self.request.user.id),
issue_id=str(issue_id),
project_id=str(project_id),
current_instance=None,
epoch=int(timezone.now().timestamp()),
notification=True,
origin=request.META.get("HTTP_ORIGIN"),
)
# Get the storage metadata
if not issue_attachment.storage_metadata:
get_asset_object_metadata.delay(str(issue_attachment.id))
issue_attachment.save()
return Response(status=status.HTTP_204_NO_CONTENT)
def get(self, request, slug, project_id, issue_id, pk=None):
if pk:
# Get the asset
asset = FileAsset.objects.get(
id=pk, workspace__slug=slug, project_id=project_id
)
# Check if the asset is uploaded
if not asset.is_uploaded:
return Response(
{"error": "The asset is not uploaded.", "status": False},
status=status.HTTP_400_BAD_REQUEST,
)
storage = S3Storage(request=request)
presigned_url = storage.generate_presigned_url(
object_name=asset.asset.name,
disposition="attachment",
filename=asset.attributes.get("name"),
)
return HttpResponseRedirect(presigned_url)
# Get all the attachments
issue_attachments = FileAsset.objects.filter(
issue_id=issue_id,
entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
workspace__slug=slug,
project_id=project_id,
is_uploaded=True,
)
# Serialize the attachments
serializer = IssueAttachmentSerializer(issue_attachments, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
def patch(self, request, slug, project_id, issue_id, pk):
issue_attachment = FileAsset.objects.get(
pk=pk, workspace__slug=slug, project_id=project_id
)
serializer = IssueAttachmentSerializer(issue_attachment)
# Send this activity only if the attachment is not uploaded before
if not issue_attachment.is_uploaded:
issue_activity.delay(
type="attachment.activity.created",
requested_data=None,
@@ -982,30 +1110,13 @@ class IssueAttachmentEndpoint(BaseAPIView):
notification=True,
origin=request.META.get("HTTP_ORIGIN"),
)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def delete(self, request, slug, project_id, issue_id, pk):
issue_attachment = FileAsset.objects.get(pk=pk)
issue_attachment.asset.delete(save=False)
issue_attachment.delete()
issue_activity.delay(
type="attachment.activity.deleted",
requested_data=None,
actor_id=str(self.request.user.id),
issue_id=str(self.kwargs.get("issue_id", None)),
project_id=str(self.kwargs.get("project_id", None)),
current_instance=None,
epoch=int(timezone.now().timestamp()),
notification=True,
origin=request.META.get("HTTP_ORIGIN"),
)
# Update the attachment
issue_attachment.is_uploaded = True
issue_attachment.created_by = request.user
# Get the storage metadata
if not issue_attachment.storage_metadata:
get_asset_object_metadata.delay(str(issue_attachment.id))
issue_attachment.save()
return Response(status=status.HTTP_204_NO_CONTENT)
def get(self, request, slug, project_id, issue_id):
issue_attachments = FileAsset.objects.filter(
issue_id=issue_id, workspace__slug=slug, project_id=project_id
)
serializer = IssueAttachmentSerializer(issue_attachments, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)