From 8c89552ab16c2d9690e62e6308ec6eea7c5bd8b9 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Mon, 3 Apr 2023 14:55:51 +0530 Subject: [PATCH] feat: issue attachments --- apiserver/plane/api/serializers/__init__.py | 1 + apiserver/plane/api/serializers/issue.py | 17 +++++++ apiserver/plane/api/urls.py | 11 +++++ apiserver/plane/api/views/__init__.py | 1 + apiserver/plane/api/views/asset.py | 2 + apiserver/plane/api/views/issue.py | 51 +++++++++++++++++++++ apiserver/plane/db/models/__init__.py | 3 +- apiserver/plane/db/models/issue.py | 37 ++++++++++++++- 8 files changed, 121 insertions(+), 2 deletions(-) diff --git a/apiserver/plane/api/serializers/__init__.py b/apiserver/plane/api/serializers/__init__.py index 57bff15c239..633ca69613a 100644 --- a/apiserver/plane/api/serializers/__init__.py +++ b/apiserver/plane/api/serializers/__init__.py @@ -41,6 +41,7 @@ IssueStateSerializer, IssueLinkSerializer, IssueLiteSerializer, + IssueAttachmentSerializer, ) from .module import ( diff --git a/apiserver/plane/api/serializers/issue.py b/apiserver/plane/api/serializers/issue.py index c5d53f8384b..34b8c16a582 100644 --- a/apiserver/plane/api/serializers/issue.py +++ b/apiserver/plane/api/serializers/issue.py @@ -25,6 +25,7 @@ Module, ModuleIssue, IssueLink, + IssueAttachment, ) @@ -439,6 +440,21 @@ def create(self, validated_data): return IssueLink.objects.create(**validated_data) +class IssueAttachmentSerializer(BaseSerializer): + class Meta: + model = IssueAttachment + fields = "__all__" + read_only_fields = [ + "created_by", + "updated_by", + "created_at", + "updated_at", + "workspace", + "project", + "issue", + ] + + # Issue Serializer with state details class IssueStateSerializer(BaseSerializer): state_detail = StateSerializer(read_only=True, source="state") @@ -466,6 +482,7 @@ class IssueSerializer(BaseSerializer): issue_cycle = IssueCycleDetailSerializer(read_only=True) issue_module = IssueModuleDetailSerializer(read_only=True) issue_link = IssueLinkSerializer(read_only=True, many=True) + issue_attachment = IssueAttachmentSerializer(read_only=True, many=True) sub_issues_count = serializers.IntegerField(read_only=True) class Meta: diff --git a/apiserver/plane/api/urls.py b/apiserver/plane/api/urls.py index d408be37e56..736d2204ae1 100644 --- a/apiserver/plane/api/urls.py +++ b/apiserver/plane/api/urls.py @@ -74,6 +74,7 @@ SubIssuesEndpoint, IssueLinkViewSet, BulkCreateIssueLabelsEndpoint, + IssueAttachmentEndpoint, ## End Issues # States StateViewSet, @@ -741,6 +742,16 @@ ), name="project-issue-links", ), + path( + "workspaces//projects//issues//issue-attachments/", + IssueAttachmentEndpoint.as_view(), + name="project-issue-attachments", + ), + path( + "workspaces//projects//issues//issue-attachments//", + IssueAttachmentEndpoint.as_view(), + name="project-issue-attachments", + ), ## End Issues ## Issue Activity path( diff --git a/apiserver/plane/api/views/__init__.py b/apiserver/plane/api/views/__init__.py index b6171d68ba4..83dd95b88fa 100644 --- a/apiserver/plane/api/views/__init__.py +++ b/apiserver/plane/api/views/__init__.py @@ -69,6 +69,7 @@ SubIssuesEndpoint, IssueLinkViewSet, BulkCreateIssueLabelsEndpoint, + IssueAttachmentEndpoint, ) from .auth_extended import ( diff --git a/apiserver/plane/api/views/asset.py b/apiserver/plane/api/views/asset.py index abdee481225..98c9f9cafdb 100644 --- a/apiserver/plane/api/views/asset.py +++ b/apiserver/plane/api/views/asset.py @@ -65,6 +65,8 @@ def delete(self, request, workspace_id, asset_key): class UserAssetsEndpoint(BaseAPIView): + parser_classes = (MultiPartParser, FormParser) + def get(self, request, asset_key): try: files = FileAsset.objects.filter(asset=asset_key, created_by=request.user) diff --git a/apiserver/plane/api/views/issue.py b/apiserver/plane/api/views/issue.py index d22c650926a..dfe3b51cc0c 100644 --- a/apiserver/plane/api/views/issue.py +++ b/apiserver/plane/api/views/issue.py @@ -12,6 +12,7 @@ # Third Party imports from rest_framework.response import Response from rest_framework import status +from rest_framework.parsers import MultiPartParser, FormParser from sentry_sdk import capture_exception # Module imports @@ -28,6 +29,7 @@ IssueFlatSerializer, IssueLinkSerializer, IssueLiteSerializer, + IssueAttachmentSerializer, ) from plane.api.permissions import ( ProjectEntityPermission, @@ -43,6 +45,7 @@ IssueProperty, Label, IssueLink, + IssueAttachment, ) from plane.bgtasks.issue_activites_task import issue_activity from plane.utils.grouper import group_results @@ -683,3 +686,51 @@ def post(self, request, slug, project_id): {"error": "Something went wrong please try again later"}, status=status.HTTP_400_BAD_REQUEST, ) + + +class IssueAttachmentEndpoint(BaseAPIView): + serializer_class = IssueAttachmentSerializer + permission_classes = [ + ProjectEntityPermission, + ] + model = IssueAttachment + parser_classes = (MultiPartParser, FormParser) + + def post(self, request, slug, project_id, issue_id): + try: + serializer = IssueAttachmentSerializer(data=request.data) + if serializer.is_valid(): + serializer.save(project_id=project_id, issue_id=issue_id) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + except Exception as e: + capture_exception(e) + return Response( + {"error": "Something went wrong please try again later"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + def delete(self, request, slug, project_id, issue_id, pk): + try: + issue_attachment = IssueAttachment.objects.get(pk=pk) + issue_attachment.delete() + return Response(status=status.HTTP_204_NO_CONTENT) + except IssueAttachment.DoesNotExist: + return Response( + {"error": "Issue Attachment does not exist"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + def get(self, request, slug, project_id, issue_id): + try: + issue_attachments = IssueAttachment.objects.filter( + issue_id=issue_id, workspace__slug=slug, project_id=project_id + ) + serilaizer = IssueAttachmentSerializer(issue_attachments, many=True) + return Response(serilaizer.data, status=status.HTTP_200_OK) + except Exception as e: + capture_exception(e) + return Response( + {"error": "Something went wrong please try again later"}, + status=status.HTTP_400_BAD_REQUEST, + ) diff --git a/apiserver/plane/db/models/__init__.py b/apiserver/plane/db/models/__init__.py index 8a302174143..46b459bbd11 100644 --- a/apiserver/plane/db/models/__init__.py +++ b/apiserver/plane/db/models/__init__.py @@ -32,6 +32,7 @@ IssueBlocker, IssueLink, IssueSequence, + IssueAttachment, ) from .asset import FileAsset @@ -61,4 +62,4 @@ from .importer import Importer -from .page import Page, PageBlock, PageFavorite, PageLabel \ No newline at end of file +from .page import Page, PageBlock, PageFavorite, PageLabel diff --git a/apiserver/plane/db/models/issue.py b/apiserver/plane/db/models/issue.py index 655a03e75ce..aeee54348b2 100644 --- a/apiserver/plane/db/models/issue.py +++ b/apiserver/plane/db/models/issue.py @@ -1,3 +1,6 @@ +# Python import +from uuid import uuid4 + # Django imports from django.contrib.postgres.fields import ArrayField from django.db import models @@ -5,6 +8,7 @@ from django.db.models.signals import post_save from django.dispatch import receiver from django.utils import timezone +from django.core.exceptions import ValidationError # Module imports from . import ProjectBaseModel @@ -54,7 +58,6 @@ class Issue(ProjectBaseModel): through_fields=("issue", "assignee"), ) sequence_id = models.IntegerField(default=1, verbose_name="Issue Sequence ID") - attachments = ArrayField(models.URLField(), size=10, blank=True, default=list) labels = models.ManyToManyField( "db.Label", blank=True, related_name="labels", through="IssueLabel" ) @@ -194,6 +197,38 @@ def __str__(self): return f"{self.issue.name} {self.url}" +def get_upload_path(instance, filename): + return f"{instance.workspace.id}/{uuid4().hex}-{filename}" + + +def file_size(value): + limit = 5 * 1024 * 1024 + if value.size > limit: + raise ValidationError("File too large. Size should not exceed 5 MB.") + + +class IssueAttachment(ProjectBaseModel): + attributes = models.JSONField(default=dict) + asset = models.FileField( + upload_to=get_upload_path, + validators=[ + file_size, + ], + ) + issue = models.ForeignKey( + "db.Issue", on_delete=models.CASCADE, related_name="issue_attachment" + ) + + class Meta: + verbose_name = "Issue Attachment" + verbose_name_plural = "Issue Attachments" + db_table = "issue_attachments" + ordering = ("-created_at",) + + def __str__(self): + return f"{self.issue.name} {self.asset}" + + class IssueActivity(ProjectBaseModel): issue = models.ForeignKey( Issue, on_delete=models.SET_NULL, null=True, related_name="issue_activity"