feat: issue attachments (#677)

This commit is contained in:
pablohashescobar 2023-04-05 00:17:55 +05:30 committed by GitHub
parent ff5cddeb95
commit 97386e9d07
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 121 additions and 2 deletions

View File

@ -41,6 +41,7 @@ from .issue import (
IssueStateSerializer, IssueStateSerializer,
IssueLinkSerializer, IssueLinkSerializer,
IssueLiteSerializer, IssueLiteSerializer,
IssueAttachmentSerializer,
) )
from .module import ( from .module import (

View File

@ -25,6 +25,7 @@ from plane.db.models import (
Module, Module,
ModuleIssue, ModuleIssue,
IssueLink, IssueLink,
IssueAttachment,
) )
@ -439,6 +440,21 @@ class IssueLinkSerializer(BaseSerializer):
return IssueLink.objects.create(**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 # Issue Serializer with state details
class IssueStateSerializer(BaseSerializer): class IssueStateSerializer(BaseSerializer):
state_detail = StateSerializer(read_only=True, source="state") state_detail = StateSerializer(read_only=True, source="state")
@ -466,6 +482,7 @@ class IssueSerializer(BaseSerializer):
issue_cycle = IssueCycleDetailSerializer(read_only=True) issue_cycle = IssueCycleDetailSerializer(read_only=True)
issue_module = IssueModuleDetailSerializer(read_only=True) issue_module = IssueModuleDetailSerializer(read_only=True)
issue_link = IssueLinkSerializer(read_only=True, many=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) sub_issues_count = serializers.IntegerField(read_only=True)
class Meta: class Meta:

View File

@ -74,6 +74,7 @@ from plane.api.views import (
SubIssuesEndpoint, SubIssuesEndpoint,
IssueLinkViewSet, IssueLinkViewSet,
BulkCreateIssueLabelsEndpoint, BulkCreateIssueLabelsEndpoint,
IssueAttachmentEndpoint,
## End Issues ## End Issues
# States # States
StateViewSet, StateViewSet,
@ -742,6 +743,16 @@ urlpatterns = [
), ),
name="project-issue-links", name="project-issue-links",
), ),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-attachments/",
IssueAttachmentEndpoint.as_view(),
name="project-issue-attachments",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-attachments/<uuid:pk>/",
IssueAttachmentEndpoint.as_view(),
name="project-issue-attachments",
),
## End Issues ## End Issues
## Issue Activity ## Issue Activity
path( path(

View File

@ -69,6 +69,7 @@ from .issue import (
SubIssuesEndpoint, SubIssuesEndpoint,
IssueLinkViewSet, IssueLinkViewSet,
BulkCreateIssueLabelsEndpoint, BulkCreateIssueLabelsEndpoint,
IssueAttachmentEndpoint,
) )
from .auth_extended import ( from .auth_extended import (

View File

@ -65,6 +65,8 @@ class FileAssetEndpoint(BaseAPIView):
class UserAssetsEndpoint(BaseAPIView): class UserAssetsEndpoint(BaseAPIView):
parser_classes = (MultiPartParser, FormParser)
def get(self, request, asset_key): def get(self, request, asset_key):
try: try:
files = FileAsset.objects.filter(asset=asset_key, created_by=request.user) files = FileAsset.objects.filter(asset=asset_key, created_by=request.user)

View File

@ -12,6 +12,7 @@ from django.views.decorators.gzip import gzip_page
# Third Party imports # Third Party imports
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.parsers import MultiPartParser, FormParser
from sentry_sdk import capture_exception from sentry_sdk import capture_exception
# Module imports # Module imports
@ -28,6 +29,7 @@ from plane.api.serializers import (
IssueFlatSerializer, IssueFlatSerializer,
IssueLinkSerializer, IssueLinkSerializer,
IssueLiteSerializer, IssueLiteSerializer,
IssueAttachmentSerializer,
) )
from plane.api.permissions import ( from plane.api.permissions import (
ProjectEntityPermission, ProjectEntityPermission,
@ -43,6 +45,7 @@ from plane.db.models import (
IssueProperty, IssueProperty,
Label, Label,
IssueLink, IssueLink,
IssueAttachment,
) )
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
@ -683,3 +686,51 @@ class BulkCreateIssueLabelsEndpoint(BaseAPIView):
{"error": "Something went wrong please try again later"}, {"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST, 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,
)

View File

@ -32,6 +32,7 @@ from .issue import (
IssueBlocker, IssueBlocker,
IssueLink, IssueLink,
IssueSequence, IssueSequence,
IssueAttachment,
) )
from .asset import FileAsset from .asset import FileAsset
@ -61,4 +62,4 @@ from .integration import (
from .importer import Importer from .importer import Importer
from .page import Page, PageBlock, PageFavorite, PageLabel from .page import Page, PageBlock, PageFavorite, PageLabel

View File

@ -1,3 +1,6 @@
# Python import
from uuid import uuid4
# Django imports # Django imports
from django.contrib.postgres.fields import ArrayField from django.contrib.postgres.fields import ArrayField
from django.db import models from django.db import models
@ -5,6 +8,7 @@ from django.conf import settings
from django.db.models.signals import post_save from django.db.models.signals import post_save
from django.dispatch import receiver from django.dispatch import receiver
from django.utils import timezone from django.utils import timezone
from django.core.exceptions import ValidationError
# Module imports # Module imports
from . import ProjectBaseModel from . import ProjectBaseModel
@ -54,7 +58,6 @@ class Issue(ProjectBaseModel):
through_fields=("issue", "assignee"), through_fields=("issue", "assignee"),
) )
sequence_id = models.IntegerField(default=1, verbose_name="Issue Sequence ID") sequence_id = models.IntegerField(default=1, verbose_name="Issue Sequence ID")
attachments = ArrayField(models.URLField(), size=10, blank=True, default=list)
labels = models.ManyToManyField( labels = models.ManyToManyField(
"db.Label", blank=True, related_name="labels", through="IssueLabel" "db.Label", blank=True, related_name="labels", through="IssueLabel"
) )
@ -194,6 +197,38 @@ class IssueLink(ProjectBaseModel):
return f"{self.issue.name} {self.url}" 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): class IssueActivity(ProjectBaseModel):
issue = models.ForeignKey( issue = models.ForeignKey(
Issue, on_delete=models.SET_NULL, null=True, related_name="issue_activity" Issue, on_delete=models.SET_NULL, null=True, related_name="issue_activity"