Merge branch 'update-file-uploads' of github.com:makeplane/plane into update-file-uploads

This commit is contained in:
pablohashescobar 2024-03-21 17:24:04 +05:30
commit 14c6cc1420
8 changed files with 294 additions and 79 deletions

View File

@ -26,3 +26,31 @@ def delete_file_asset():
file_asset.asset.delete(save=False) file_asset.asset.delete(save=False)
# Delete the file object # Delete the file object
file_asset.delete() file_asset.delete()
@shared_task
def file_asset_size(slug, email, members, issue_count, cycle_count, module_count):
asset_size = []
# s3_client = boto3.client('s3')
assets_to_update = []
# for asset in FileAsset.objects.filter(size__isnull=True):
# try:
# key = f"{workspace_id}/{asset_key}"
# response = s3_client.head_object(Bucket=settings.AWS_STORAGE_BUCKET_NAME, Key=key)
# size = response['ContentLength']
# asset.size = size
# assets_to_update.append(asset)
# except Exception as e:
# # Handle exceptions such as S3 object not found
# print(f"Error updating asset size for {asset.asset.key}: {e}")
# # Bulk update only objects that need updating
# FileAsset.objects.bulk_update(assets_to_update, ["size"], batch_size=50)
for asset in FileAsset.objects.filter(size__isnull=True):
asset.size = asset.asset.size
asset_size.append(asset)
FileAsset.objects.bulk_update(asset_size, ["size"], batch_size=50)
print("File asset size updated successfully")

View File

@ -0,0 +1,23 @@
# Python imports
import getpass
# Django imports
from django.core.management import BaseCommand
# Module imports
from plane.db.models import User
class Command(BaseCommand):
help = "Check the file asset size of the file"
def handle(self, *args, **options):
from plane.bgtasks.file_asset_task import file_asset_size
file_asset_size.delay()
self.stdout.write(
self.style.SUCCESS(f"File asset size pushed to queue")
)

View File

@ -33,7 +33,7 @@ def convert_issue_description_image_sources(apps, schema_editor):
src = img.get("src", "") src = img.get("src", "")
if src and (src.startswith(prefix1)): if src and (src.startswith(prefix1)):
img["src"] = ( img["src"] = (
f"/api/workspaces/{issue.workspace.slug}/projects/{issue.project_id}/issues/{issue.id}/attachments/{src[len(prefix1): ]}" f"{src[len(prefix1): ]}"
) )
file_assets[src[len(prefix1) :]] = { file_assets[src[len(prefix1) :]] = {
"project_id": str(issue.project_id), "project_id": str(issue.project_id),
@ -45,7 +45,7 @@ def convert_issue_description_image_sources(apps, schema_editor):
# prefix 2 # prefix 2
if not settings.USE_MINIO and src and src.startswith(prefix2): if not settings.USE_MINIO and src and src.startswith(prefix2):
img["src"] = ( img["src"] = (
f"/api/workspaces/{issue.workspace.slug}/projects/{issue.project_id}/issues/{issue.id}/attachments/{src[len(prefix2): ]}" f"{src[len(prefix2): ]}"
) )
file_assets[src[len(prefix2) :]] = { file_assets[src[len(prefix2) :]] = {
"project_id": str(issue.project_id), "project_id": str(issue.project_id),
@ -109,7 +109,7 @@ def convert_page_image_sources(apps, schema_editor):
src = img.get("src", "") src = img.get("src", "")
if src and (src.startswith(prefix1)): if src and (src.startswith(prefix1)):
img["src"] = ( img["src"] = (
f"/api/workspaces/{page.workspace.slug}/projects/{page.project_id}/issues/{page.id}/attachments/{src[len(prefix1): ]}/" f"{src[len(prefix1): ]}/"
) )
file_assets[src[len(prefix1) :]] = { file_assets[src[len(prefix1) :]] = {
"project_id": str(page.project_id), "project_id": str(page.project_id),
@ -121,7 +121,7 @@ def convert_page_image_sources(apps, schema_editor):
# prefix 2 # prefix 2
if not settings.USE_MINIO and src and src.startswith(prefix2): if not settings.USE_MINIO and src and src.startswith(prefix2):
img["src"] = ( img["src"] = (
f"/api/workspaces/{page.workspace.slug}/projects/{page.project_id}/issues/{page.id}/attachments/{src[len(prefix2): ]}/" f"{src[len(prefix2): ]}/"
) )
file_assets[src[len(prefix2) :]] = { file_assets[src[len(prefix2) :]] = {
"project_id": str(page.project_id), "project_id": str(page.project_id),
@ -181,7 +181,7 @@ def convert_comment_image_sources(apps, schema_editor):
src = img.get("src", "") src = img.get("src", "")
if src and (src.startswith(prefix1)): if src and (src.startswith(prefix1)):
img["src"] = ( img["src"] = (
f"/api/workspaces/{comment.workspace.slug}/projects/{comment.project_id}/issues/{comment.id}/attachments/{src[len(prefix1): ]}/" f"{src[len(prefix1): ]}/"
) )
file_assets[src[len(prefix1) :]] = { file_assets[src[len(prefix1) :]] = {
"project_id": str(comment.project_id), "project_id": str(comment.project_id),
@ -193,7 +193,7 @@ def convert_comment_image_sources(apps, schema_editor):
# prefix 2 # prefix 2
if not settings.USE_MINIO and src and src.startswith(prefix2): if not settings.USE_MINIO and src and src.startswith(prefix2):
img["src"] = ( img["src"] = (
f"/api/workspaces/{comment.workspace.slug}/projects/{comment.project_id}/issues/{comment.id}/attachments/{src[len(prefix2): ]}/" f"{src[len(prefix2): ]}/"
) )
file_assets[src[len(prefix2) :]] = { file_assets[src[len(prefix2) :]] = {
"project_id": str(comment.project_id), "project_id": str(comment.project_id),

View File

@ -3,3 +3,5 @@ from .user import UserLiteSerializer
from .issue import LabelLiteSerializer, StateLiteSerializer from .issue import LabelLiteSerializer, StateLiteSerializer
from .state import StateSerializer, StateLiteSerializer from .state import StateSerializer, StateLiteSerializer
from .asset import FileAssetSerializer

View File

@ -0,0 +1,15 @@
from .base import BaseFileSerializer
from plane.db.models import FileAsset
class FileAssetSerializer(BaseFileSerializer):
class Meta:
model = FileAsset
fields = "__all__"
read_only_fields = [
"created_by",
"updated_by",
"created_at",
"updated_at",
]

View File

@ -56,3 +56,22 @@ class DynamicBaseSerializer(BaseSerializer):
self.fields.pop(field_name) self.fields.pop(field_name)
return self.fields return self.fields
class BaseFileSerializer(DynamicBaseSerializer):
class Meta:
abstract = True # Make this serializer abstract
def to_representation(self, instance):
"""
Object instance -> Dict of primitive datatypes.
"""
response = super().to_representation(instance)
response[
"asset"
] = (
instance.asset.name
) # Ensure 'asset' field is consistently serialized
# Apply custom method to get download URL
return response

View File

@ -36,6 +36,26 @@ urlpatterns = [
), ),
name="issue-comments-project-board", name="issue-comments-project-board",
), ),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/attachments/",
IssueAttachmentPublicEndpoint.as_view(),
name="project-issue-attachments",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/attachments/<uuid:workspace_id>/<str:asset_key>/",
IssueAttachmentPublicEndpoint.as_view(),
name="project-issue-attachments",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/comments/<uuid:comment_id>/attachments/",
CommentAssetPublicEndpoint.as_view(),
name="issue-comments-project-board-attachments",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/comments/<uuid:comment_id>/attachments/<uuid:workspace_id>/<str:asset_key>/",
CommentAssetPublicEndpoint.as_view(),
name="issue-comments-project-board-attachments",
),
path( path(
"workspaces/<str:slug>/project-boards/<uuid:project_id>/issues/<uuid:issue_id>/reactions/", "workspaces/<str:slug>/project-boards/<uuid:project_id>/issues/<uuid:issue_id>/reactions/",
IssueReactionPublicViewSet.as_view( IssueReactionPublicViewSet.as_view(

View File

@ -1,7 +1,6 @@
# Python imports # Python imports
import json import json
# Django imports
from django.core.serializers.json import DjangoJSONEncoder from django.core.serializers.json import DjangoJSONEncoder
from django.db.models import ( from django.db.models import (
Case, Case,
@ -18,10 +17,13 @@ from django.db.models import (
When, When,
) )
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
# Django imports
from django.utils import timezone from django.utils import timezone
# Third Party imports # Third Party imports
from rest_framework import status from rest_framework import status
from rest_framework.parsers import FormParser, JSONParser, MultiPartParser
from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.response import Response from rest_framework.response import Response
@ -34,6 +36,7 @@ from plane.app.serializers import (
IssueReactionSerializer, IssueReactionSerializer,
IssueVoteSerializer, IssueVoteSerializer,
) )
from plane.app.views.base import BaseAPIView, BaseViewSet
from plane.bgtasks.issue_activites_task import issue_activity from plane.bgtasks.issue_activites_task import issue_activity
from plane.db.models import ( from plane.db.models import (
CommentReaction, CommentReaction,
@ -48,13 +51,12 @@ from plane.db.models import (
ProjectMember, ProjectMember,
ProjectPublicMember, ProjectPublicMember,
State, State,
Workspace,
) )
from plane.utils.grouper import group_results from plane.utils.grouper import group_results
from plane.utils.issue_filters import issue_filters from plane.utils.issue_filters import issue_filters
from plane.utils.presigned_url_generator import generate_download_presigned_url from plane.utils.presigned_url_generator import generate_download_presigned_url
from .base import BaseAPIView, BaseViewSet
class IssueCommentPublicViewSet(BaseViewSet): class IssueCommentPublicViewSet(BaseViewSet):
serializer_class = IssueCommentSerializer serializer_class = IssueCommentSerializer
@ -218,6 +220,182 @@ class IssueCommentPublicViewSet(BaseViewSet):
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
class IssueAttachmentPublicEndpoint(BaseAPIView):
def get_permissions(self):
if self.action in ["get"]:
self.permission_classes = [
AllowAny,
]
else:
self.permission_classes = [
IsAuthenticated,
]
return super(IssueAttachmentPublicEndpoint, self).get_permissions()
parser_classes = (
MultiPartParser,
FormParser,
JSONParser,
)
def post(self, request, slug, project_id, issue_id):
serializer = FileAssetSerializer(data=request.data)
workspace = Workspace.objects.get(slug=slug)
if serializer.is_valid():
serializer.save(
workspace=workspace,
project_id=project_id,
entity_identifier=issue_id,
)
issue_activity.delay(
type="attachment.activity.created",
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=json.dumps(
serializer.data,
cls=DjangoJSONEncoder,
),
epoch=int(timezone.now().timestamp()),
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, workspace_id, asset_key
):
key = f"{workspace_id}/{asset_key}"
asset = FileAsset.objects.get(
asset=key,
entity_identifier=issue_id,
entity_type="issue_attachment",
workspace__slug=slug,
project_id=project_id,
)
asset.is_deleted = True
asset.save()
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"),
)
return Response(status=status.HTTP_204_NO_CONTENT)
def get(
self,
request,
slug,
project_id,
issue_id,
workspace_id=None,
asset_key=None,
):
if workspace_id and asset_key:
key = f"{workspace_id}/{asset_key}"
url = generate_download_presigned_url(
key=key,
host=request.get_host(),
scheme=request.scheme,
)
return HttpResponseRedirect(url)
# For listing
issue_attachments = FileAsset.objects.filter(
entity_type="issue_attachment",
entity_identifier=issue_id,
workspace__slug=slug,
project_id=project_id,
)
serializer = FileAssetSerializer(issue_attachments, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
class CommentAssetPublicEndpoint(BaseAPIView):
def get_permissions(self):
if self.action in ["get"]:
self.permission_classes = [
AllowAny,
]
else:
self.permission_classes = [
IsAuthenticated,
]
return super(CommentAssetPublicEndpoint, self).get_permissions()
parser_classes = (
MultiPartParser,
FormParser,
JSONParser,
)
def post(self, request, slug, project_id, comment_id):
serializer = FileAssetSerializer(data=request.data)
workspace = Workspace.objects.get(slug=slug)
if serializer.is_valid():
serializer.save(
workspace=workspace,
project_id=project_id,
entity_type="comment",
entity_identifier=comment_id,
)
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, comment_id, workspace_id, asset_key
):
key = f"{workspace_id}/{asset_key}"
asset = FileAsset.objects.get(
asset=key,
entity_identifier=comment_id,
entity_type="comment",
workspace__slug=slug,
project_id=project_id,
)
asset.is_deleted = True
asset.save()
return Response(status=status.HTTP_204_NO_CONTENT)
def get(
self,
request,
slug,
project_id,
comment_id,
workspace_id=None,
asset_key=None,
):
if workspace_id and asset_key:
key = f"{workspace_id}/{asset_key}"
url = generate_download_presigned_url(
key=key,
host=request.get_host(),
scheme=request.scheme,
)
return HttpResponseRedirect(url)
# For listing
comment_assets = FileAsset.objects.filter(
entity_type="comment",
entity_identifier=comment_id,
workspace__slug=slug,
project_id=project_id,
)
serializer = FileAssetSerializer(comment_assets, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
class IssueReactionPublicViewSet(BaseViewSet): class IssueReactionPublicViewSet(BaseViewSet):
serializer_class = IssueReactionSerializer serializer_class = IssueReactionSerializer
model = IssueReaction model = IssueReaction
@ -690,73 +868,3 @@ class ProjectIssuesPublicEndpoint(BaseAPIView):
}, },
status=status.HTTP_200_OK, status=status.HTTP_200_OK,
) )
class IssueAttachmentPublicEndpoint(BaseAPIView):
permission_classes = [
AllowAny,
]
def get(
self,
request,
slug,
project_id,
issue_id,
workspace_id=None,
asset_key=None,
):
if workspace_id and asset_key:
key = f"{workspace_id}/{asset_key}"
url = generate_download_presigned_url(
key=key,
host=request.get_host(),
scheme=request.scheme,
)
return HttpResponseRedirect(url)
# For listing
issue_attachments = FileAsset.objects.filter(
entity_type="issue_attachment",
entity_identifier=issue_id,
workspace__slug=slug,
project_id=project_id,
)
serializer = FileAssetSerializer(issue_attachments, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
class CommentAssetPublicEndpoint(BaseAPIView):
permission_classes = [
AllowAny,
]
def get(
self,
request,
slug,
project_id,
comment_id,
workspace_id=None,
asset_key=None,
):
if workspace_id and asset_key:
key = f"{workspace_id}/{asset_key}"
url = generate_download_presigned_url(
key=key,
host=request.get_host(),
scheme=request.scheme,
)
return HttpResponseRedirect(url)
# For listing
comment_assets = FileAsset.objects.filter(
entity_type="comment",
entity_identifier=comment_id,
workspace__slug=slug,
project_id=project_id,
)
serializer = FileAssetSerializer(comment_assets, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)