Skip to content

Commit

Permalink
Merge branch 'preview' into feat/flat-lists
Browse files Browse the repository at this point in the history
  • Loading branch information
Palanikannan1437 committed Jan 6, 2025
2 parents 151dc42 + 625cbf8 commit e094b49
Show file tree
Hide file tree
Showing 137 changed files with 3,747 additions and 1,111 deletions.
179 changes: 145 additions & 34 deletions apiserver/plane/api/views/issue.py
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -19,11 +20,11 @@
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 (
Expand All @@ -50,8 +51,10 @@
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


Expand Down Expand Up @@ -940,72 +943,180 @@ 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)
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)
# 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)
issue_attachment.asset.delete(save=False)
issue_attachment.delete()
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(self.kwargs.get("issue_id", None)),
project_id=str(self.kwargs.get("project_id", None)),
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):
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, workspace__slug=slug, project_id=project_id
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,
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"),
)

# 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)
3 changes: 2 additions & 1 deletion apiserver/plane/app/serializers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
WorkspaceMemberAdminSerializer,
WorkspaceMemberMeSerializer,
WorkspaceUserPropertiesSerializer,
WorkspaceUserLinkSerializer
WorkspaceUserLinkSerializer,
WorkspaceRecentVisitSerializer
)
from .project import (
ProjectSerializer,
Expand Down
1 change: 0 additions & 1 deletion apiserver/plane/app/serializers/favorite.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ def get_entity_model_and_serializer(entity_type):
}
return entity_map.get(entity_type, (None, None))


class UserFavoriteSerializer(serializers.ModelSerializer):
entity_data = serializers.SerializerMethodField()

Expand Down
95 changes: 94 additions & 1 deletion apiserver/plane/app/serializers/workspace.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
# Third party imports
from rest_framework import serializers
from rest_framework import status
from rest_framework.response import Response

# Module imports
from .base import BaseSerializer, DynamicBaseSerializer
from .user import UserLiteSerializer, UserAdminLiteSerializer


from plane.db.models import (
Workspace,
WorkspaceMember,
WorkspaceMemberInvite,
WorkspaceTheme,
WorkspaceUserProperties,
WorkspaceUserLink
WorkspaceUserLink,
UserRecentVisit,
Issue,
Page,
Project,
ProjectMember
)
from plane.utils.constants import RESTRICTED_WORKSPACE_SLUGS

Expand Down Expand Up @@ -132,3 +140,88 @@ def validate_url(self, value):
raise serializers.ValidationError({"error": "Invalid URL format."})

return value

class IssueRecentVisitSerializer(serializers.ModelSerializer):
project_identifier = serializers.SerializerMethodField()

class Meta:
model = Issue
fields = ["name", "state", "priority", "assignees", "type", "sequence_id", "project_id", "project_identifier"]

def get_project_identifier(self, obj):
project = obj.project

return project.identifier if project else None

class ProjectMemberSerializer(BaseSerializer):
member = UserLiteSerializer(read_only=True)

class Meta:
model = ProjectMember
fields = ["member"]

class ProjectRecentVisitSerializer(serializers.ModelSerializer):
project_members = serializers.SerializerMethodField()

class Meta:
model = Project
fields = ["id", "name", "logo_props", "project_members", "identifier"]

def get_project_members(self, obj):
members = ProjectMember.objects.filter(project_id=obj.id).select_related('member')

serializer = ProjectMemberSerializer(members, many=True)
return serializer.data

class PageRecentVisitSerializer(serializers.ModelSerializer):
project_id = serializers.SerializerMethodField()
project_identifier = serializers.SerializerMethodField()

class Meta:
model = Page
fields = ["id", "name", "logo_props", "project_id", "owned_by", "project_identifier"]

def get_project_id(self, obj):
return obj.project_id if hasattr(obj, 'project_id') else obj.projects.values_list('id', flat=True).first()

def get_project_identifier(self, obj):
project = obj.projects.first()

return project.identifier if project else None

def get_entity_model_and_serializer(entity_type):
entity_map = {
"issue": (Issue, IssueRecentVisitSerializer),
"page": (Page, PageRecentVisitSerializer),
"project": (Project, ProjectRecentVisitSerializer)
}
return entity_map.get(entity_type, (None, None))

class WorkspaceRecentVisitSerializer(BaseSerializer):
entity_data = serializers.SerializerMethodField()

class Meta:
model = UserRecentVisit
fields = [
"id",
"entity_name",
"entity_identifier",
"entity_data",
"visited_at"
]
read_only_fields = ["workspace", "owner", "created_by", "updated_by"]

def get_entity_data(self, obj):
entity_name = obj.entity_name
entity_identifier = obj.entity_identifier

entity_model, entity_serializer = get_entity_model_and_serializer(entity_name)

if entity_model and entity_serializer:
try:
entity = entity_model.objects.get(pk=entity_identifier)

return entity_serializer(entity).data
except entity_model.DoesNotExist:
return None
return None
Loading

0 comments on commit e094b49

Please sign in to comment.