feat: add issue attachment external endpoint (#6307)
This commit is contained in:
parent
d6bcd8dd15
commit
3fd2550d69
1 changed files with 149 additions and 38 deletions
|
|
@ -1,9 +1,10 @@
|
||||||
# Python imports
|
# Python imports
|
||||||
import json
|
import json
|
||||||
|
import uuid
|
||||||
from django.core.serializers.json import DjangoJSONEncoder
|
|
||||||
|
|
||||||
# Django imports
|
# Django imports
|
||||||
|
from django.core.serializers.json import DjangoJSONEncoder
|
||||||
|
from django.http import HttpResponseRedirect
|
||||||
from django.db import IntegrityError
|
from django.db import IntegrityError
|
||||||
from django.db.models import (
|
from django.db.models import (
|
||||||
Case,
|
Case,
|
||||||
|
|
@ -19,11 +20,11 @@ from django.db.models import (
|
||||||
Subquery,
|
Subquery,
|
||||||
)
|
)
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
# Third party imports
|
# Third party imports
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.parsers import MultiPartParser, FormParser
|
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from plane.api.serializers import (
|
from plane.api.serializers import (
|
||||||
|
|
@ -50,8 +51,10 @@ from plane.db.models import (
|
||||||
Project,
|
Project,
|
||||||
ProjectMember,
|
ProjectMember,
|
||||||
CycleIssue,
|
CycleIssue,
|
||||||
|
Workspace,
|
||||||
)
|
)
|
||||||
|
from plane.settings.storage import S3Storage
|
||||||
|
from plane.bgtasks.storage_metadata_task import get_asset_object_metadata
|
||||||
from .base import BaseAPIView
|
from .base import BaseAPIView
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -940,37 +943,162 @@ class IssueAttachmentEndpoint(BaseAPIView):
|
||||||
serializer_class = IssueAttachmentSerializer
|
serializer_class = IssueAttachmentSerializer
|
||||||
permission_classes = [ProjectEntityPermission]
|
permission_classes = [ProjectEntityPermission]
|
||||||
model = FileAsset
|
model = FileAsset
|
||||||
parser_classes = (MultiPartParser, FormParser)
|
|
||||||
|
|
||||||
def post(self, request, slug, project_id, issue_id):
|
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 (
|
if (
|
||||||
request.data.get("external_id")
|
request.data.get("external_id")
|
||||||
and request.data.get("external_source")
|
and request.data.get("external_source")
|
||||||
and FileAsset.objects.filter(
|
and FileAsset.objects.filter(
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
workspace__slug=slug,
|
workspace__slug=slug,
|
||||||
issue_id=issue_id,
|
|
||||||
external_source=request.data.get("external_source"),
|
external_source=request.data.get("external_source"),
|
||||||
external_id=request.data.get("external_id"),
|
external_id=request.data.get("external_id"),
|
||||||
|
issue_id=issue_id,
|
||||||
|
entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
|
||||||
).exists()
|
).exists()
|
||||||
):
|
):
|
||||||
issue_attachment = FileAsset.objects.filter(
|
asset = FileAsset.objects.filter(
|
||||||
workspace__slug=slug,
|
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
external_id=request.data.get("external_id"),
|
workspace__slug=slug,
|
||||||
external_source=request.data.get("external_source"),
|
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()
|
).first()
|
||||||
return Response(
|
return Response(
|
||||||
{
|
{
|
||||||
"error": "Issue attachment with the same external id and external source already exists",
|
"error": "Issue with the same external id and external source already exists",
|
||||||
"id": str(issue_attachment.id),
|
"id": str(asset.id),
|
||||||
},
|
},
|
||||||
status=status.HTTP_409_CONFLICT,
|
status=status.HTTP_409_CONFLICT,
|
||||||
)
|
)
|
||||||
|
|
||||||
if serializer.is_valid():
|
# Create a File Asset
|
||||||
serializer.save(project_id=project_id, issue_id=issue_id)
|
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(
|
issue_activity.delay(
|
||||||
type="attachment.activity.created",
|
type="attachment.activity.created",
|
||||||
requested_data=None,
|
requested_data=None,
|
||||||
|
|
@ -982,30 +1110,13 @@ class IssueAttachmentEndpoint(BaseAPIView):
|
||||||
notification=True,
|
notification=True,
|
||||||
origin=request.META.get("HTTP_ORIGIN"),
|
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):
|
# Update the attachment
|
||||||
issue_attachment = FileAsset.objects.get(pk=pk)
|
issue_attachment.is_uploaded = True
|
||||||
issue_attachment.asset.delete(save=False)
|
issue_attachment.created_by = request.user
|
||||||
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"),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
# 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)
|
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)
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue