mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
chore: issue attachment and description endpoint for space
This commit is contained in:
parent
79685b33d6
commit
6719738a9f
apiserver/plane
bgtasks
db
space
@ -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")
|
||||||
|
23
apiserver/plane/db/management/commands/file_asset_size.py
Normal file
23
apiserver/plane/db/management/commands/file_asset_size.py
Normal 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")
|
||||||
|
)
|
@ -35,7 +35,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),
|
||||||
@ -47,7 +47,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),
|
||||||
@ -110,7 +110,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),
|
||||||
@ -122,7 +122,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),
|
||||||
|
@ -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
|
15
apiserver/plane/space/serializer/asset.py
Normal file
15
apiserver/plane/space/serializer/asset.py
Normal 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",
|
||||||
|
]
|
@ -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
|
@ -6,6 +6,8 @@ from plane.space.views import (
|
|||||||
IssueCommentPublicViewSet,
|
IssueCommentPublicViewSet,
|
||||||
IssueReactionPublicViewSet,
|
IssueReactionPublicViewSet,
|
||||||
CommentReactionPublicViewSet,
|
CommentReactionPublicViewSet,
|
||||||
|
CommentAssetPublicEndpoint,
|
||||||
|
IssueAttachmentPublicEndpoint
|
||||||
)
|
)
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
@ -35,6 +37,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(
|
||||||
|
@ -10,6 +10,8 @@ from .issue import (
|
|||||||
IssueVotePublicViewSet,
|
IssueVotePublicViewSet,
|
||||||
IssueRetrievePublicEndpoint,
|
IssueRetrievePublicEndpoint,
|
||||||
ProjectIssuesPublicEndpoint,
|
ProjectIssuesPublicEndpoint,
|
||||||
|
CommentAssetPublicEndpoint,
|
||||||
|
IssueAttachmentPublicEndpoint,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .inbox import InboxIssuePublicViewSet
|
from .inbox import InboxIssuePublicViewSet
|
||||||
|
@ -3,6 +3,7 @@ import json
|
|||||||
|
|
||||||
# Django imports
|
# Django imports
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from django.http import HttpResponseRedirect
|
||||||
from django.db.models import (
|
from django.db.models import (
|
||||||
Prefetch,
|
Prefetch,
|
||||||
OuterRef,
|
OuterRef,
|
||||||
@ -24,6 +25,8 @@ from django.core.serializers.json import DjangoJSONEncoder
|
|||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.permissions import AllowAny, IsAuthenticated
|
from rest_framework.permissions import AllowAny, IsAuthenticated
|
||||||
|
from rest_framework.parsers import MultiPartParser, FormParser, JSONParser
|
||||||
|
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from .base import BaseViewSet, BaseAPIView
|
from .base import BaseViewSet, BaseAPIView
|
||||||
@ -33,6 +36,7 @@ from plane.app.serializers import (
|
|||||||
CommentReactionSerializer,
|
CommentReactionSerializer,
|
||||||
IssueVoteSerializer,
|
IssueVoteSerializer,
|
||||||
IssuePublicSerializer,
|
IssuePublicSerializer,
|
||||||
|
FileAssetSerializer,
|
||||||
)
|
)
|
||||||
|
|
||||||
from plane.db.models import (
|
from plane.db.models import (
|
||||||
@ -48,10 +52,12 @@ from plane.db.models import (
|
|||||||
ProjectDeployBoard,
|
ProjectDeployBoard,
|
||||||
IssueVote,
|
IssueVote,
|
||||||
ProjectPublicMember,
|
ProjectPublicMember,
|
||||||
|
Workspace,
|
||||||
)
|
)
|
||||||
from plane.bgtasks.issue_activites_task import issue_activity
|
from plane.bgtasks.issue_activites_task import issue_activity
|
||||||
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
|
||||||
|
|
||||||
|
|
||||||
class IssueCommentPublicViewSet(BaseViewSet):
|
class IssueCommentPublicViewSet(BaseViewSet):
|
||||||
@ -216,6 +222,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
|
||||||
|
Loading…
Reference in New Issue
Block a user