[MOBIL-646] chore: add issue attachment mutations and update enums for workspace and project asset mutations (#2094)

* chore: issue attachements queries and mutations

* chore: updated attachment id in Issue attachment
This commit is contained in:
guru_sainath
2025-01-03 16:48:05 +05:30
committed by GitHub
parent e8636aac94
commit fe077f4846
14 changed files with 397 additions and 179 deletions

View File

@@ -24,7 +24,7 @@ from plane.settings.storage import S3Storage
from plane.graphql.types.asset import (
FileAssetType,
AssetPresignedUrlResponseType,
UserAssetFileEnumType,
FileAssetAssetType,
ProjectAssetEnumType,
)
from plane.bgtasks.storage_metadata_task import get_asset_object_metadata
@@ -63,21 +63,18 @@ def get_entity_id_field(entity_type, entity_identifier):
if entity_type == ProjectAssetEnumType.PROJECT_COVER.value:
return {"project_id": entity_identifier}
if entity_type in [
ProjectAssetEnumType.ISSUE_ATTACHMENT.value,
ProjectAssetEnumType.ISSUE_DESCRIPTION.value,
]:
return {"issue_id": entity_identifier}
if entity_type == ProjectAssetEnumType.PAGE_DESCRIPTION.value:
return {"page_id": entity_identifier}
if entity_type == ProjectAssetEnumType.COMMENT_DESCRIPTION.value:
return {"comment_id": entity_identifier}
if entity_type == ProjectAssetEnumType.ISSUE_DESCRIPTION.value:
return {"issue_id": entity_identifier}
if entity_type == ProjectAssetEnumType.DRAFT_ISSUE_DESCRIPTION.value:
return {"draft_issue_id": entity_identifier}
if entity_type == ProjectAssetEnumType.COMMENT_DESCRIPTION.value:
return {"comment_id": entity_identifier}
return {}
@@ -131,11 +128,12 @@ class ProjectAssetMutation:
# Checking if the entity type is valid
if not entity_type or entity_type not in [
ProjectAssetEnumType.PROJECT_COVER,
ProjectAssetEnumType.ISSUE_ATTACHMENT,
ProjectAssetEnumType.ISSUE_DESCRIPTION,
ProjectAssetEnumType.COMMENT_DESCRIPTION,
ProjectAssetEnumType.PAGE_DESCRIPTION,
ProjectAssetEnumType.ISSUE_DESCRIPTION,
ProjectAssetEnumType.DRAFT_ISSUE_DESCRIPTION,
ProjectAssetEnumType.ISSUE_ATTACHMENT,
ProjectAssetEnumType.DRAFT_ISSUE_ATTACHMENT,
ProjectAssetEnumType.COMMENT_DESCRIPTION,
]:
message = "Invalid entity type."
error_extensions = {"code": "INVALID_ENTITY_TYPE", "statusCode": 400}
@@ -143,11 +141,11 @@ class ProjectAssetMutation:
# Checking if the file type is valid
allowed_types = [
UserAssetFileEnumType.IMAGE_JPEG.value,
UserAssetFileEnumType.IMAGE_PNG.value,
UserAssetFileEnumType.IMAGE_WEBP.value,
UserAssetFileEnumType.IMAGE_JPG.value,
UserAssetFileEnumType.IMAGE_GIF.value,
FileAssetAssetType.IMAGE_JPEG.value,
FileAssetAssetType.IMAGE_PNG.value,
FileAssetAssetType.IMAGE_WEBP.value,
FileAssetAssetType.IMAGE_JPG.value,
FileAssetAssetType.IMAGE_GIF.value,
]
if type not in allowed_types:
message = "Invalid file type. Only JPEG and PNG files are allowed."
@@ -283,24 +281,24 @@ class ProjectAssetMutation:
project_details = await get_project(project)
await save_project_cover(project_details, asset.id)
if asset.entity_type == ProjectAssetEnumType.ISSUE_DESCRIPTION.value:
for asset in assets:
asset.issue_id = entity_id
await sync_to_async(asset.save)(update_fields=["issue_id"])
if asset.entity_type == ProjectAssetEnumType.COMMENT_DESCRIPTION.value:
for asset in assets:
asset.comment_id = entity_id
await sync_to_async(asset.save)(update_fields=["comment_id"])
if asset.entity_type == ProjectAssetEnumType.PAGE_DESCRIPTION.value:
for asset in assets:
asset.page_id = entity_id
await sync_to_async(asset.save)(update_fields=["page_id"])
if asset.entity_type == ProjectAssetEnumType.ISSUE_DESCRIPTION.value:
for asset in assets:
asset.issue_id = entity_id
await sync_to_async(asset.save)(update_fields=["issue_id"])
if asset.entity_type == ProjectAssetEnumType.DRAFT_ISSUE_DESCRIPTION.value:
for asset in assets:
asset.draft_issue_id = entity_id
await sync_to_async(asset.save)(update_fields=["draft_issue_id"])
if asset.entity_type == ProjectAssetEnumType.COMMENT_DESCRIPTION.value:
for asset in assets:
asset.comment_id = entity_id
await sync_to_async(asset.save)(update_fields=["comment_id"])
return True

View File

@@ -23,7 +23,7 @@ from plane.settings.storage import S3Storage
from plane.graphql.types.asset import (
FileAssetType,
AssetPresignedUrlResponseType,
UserAssetFileEnumType,
FileAssetAssetType,
UserAssetEnumType,
)
from plane.bgtasks.storage_metadata_task import get_asset_object_metadata
@@ -127,11 +127,11 @@ class UserAssetMutation:
# Checking if the file type is valid
allowed_types = [
UserAssetFileEnumType.IMAGE_JPEG.value,
UserAssetFileEnumType.IMAGE_PNG.value,
UserAssetFileEnumType.IMAGE_WEBP.value,
UserAssetFileEnumType.IMAGE_JPG.value,
UserAssetFileEnumType.IMAGE_GIF.value,
FileAssetAssetType.IMAGE_JPEG.value,
FileAssetAssetType.IMAGE_PNG.value,
FileAssetAssetType.IMAGE_WEBP.value,
FileAssetAssetType.IMAGE_JPG.value,
FileAssetAssetType.IMAGE_GIF.value,
]
if type not in allowed_types:
message = "Invalid file type. Only JPEG and PNG files are allowed."

View File

@@ -24,7 +24,7 @@ from plane.settings.storage import S3Storage
from plane.graphql.types.asset import (
FileAssetType,
AssetPresignedUrlResponseType,
UserAssetFileEnumType,
FileAssetAssetType,
WorkspaceAssetEnumType,
)
from plane.bgtasks.storage_metadata_task import get_asset_object_metadata
@@ -187,11 +187,11 @@ class WorkspaceAssetMutation:
# Checking if the file type is valid
allowed_types = [
UserAssetFileEnumType.IMAGE_JPEG.value,
UserAssetFileEnumType.IMAGE_PNG.value,
UserAssetFileEnumType.IMAGE_WEBP.value,
UserAssetFileEnumType.IMAGE_JPG.value,
UserAssetFileEnumType.IMAGE_GIF.value,
FileAssetAssetType.IMAGE_JPEG.value,
FileAssetAssetType.IMAGE_PNG.value,
FileAssetAssetType.IMAGE_WEBP.value,
FileAssetAssetType.IMAGE_JPG.value,
FileAssetAssetType.IMAGE_GIF.value,
]
if type not in allowed_types:
message = "Invalid file type. Only JPEG and PNG files are allowed."

View File

@@ -439,48 +439,6 @@ class IssueUserPropertyMutation:
return issue_properties
@strawberry.type
class IssueAttachmentMutation:
# @strawberry.mutation(
# extensions=[PermissionExtension(permissions=[ProjectBasePermission()])]
# )
# def upload_file(self, file: Upload, info: Info) -> bool:
# content = file.read()
# filename = file.filename
# @strawberry.mutation(
# extensions=[PermissionExtension(permissions=[ProjectBasePermission()])]
# )
# async def upload_file(self, file: Upload, info: Info) -> bool:
# content = await sync_to_async(file.read)()
# filename = file.filename
# # Save the file using Django's file storage
# await sync_to_async(default_storage.save)(filename, content)
# return True
@strawberry.mutation(
extensions=[PermissionExtension(permissions=[ProjectBasePermission()])]
)
async def deleteIssueAttachment(
self,
info: Info,
slug: str,
project: strawberry.ID,
issue: strawberry.ID,
attachment: strawberry.ID,
) -> bool:
issue_attachment = await sync_to_async(FileAsset.objects.get)(
id=attachment,
entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
entity_identifier=issue,
project_id=project,
workspace__slug=slug,
)
await sync_to_async(issue_attachment.delete)()
return True
@strawberry.type
class IssueSubscriptionMutation:
@strawberry.mutation(

View File

@@ -4,3 +4,4 @@ from .sub_issue import SubIssueMutation
from .cycle import IssueCycleMutation
from .module import IssueModuleMutation
from .links import IssueLinkMutation
from .attachment import IssueAttachmentMutation

View File

@@ -0,0 +1,235 @@
# Python imports
import json
import requests
import uuid
from typing import Optional
# Django imports
from django.conf import settings
from django.utils import timezone
# Third-Party Imports
import strawberry
from strawberry.scalars import JSON
from strawberry.exceptions import GraphQLError
# Python Standard Library Imports
from asgiref.sync import sync_to_async
# Strawberry Imports
from strawberry.types import Info
from strawberry.permission import PermissionExtension
# Module Imports
from plane.settings.storage import S3Storage
from plane.db.models import FileAsset, Workspace
from plane.graphql.types.asset import FileAssetType, FileAssetEntityType
from plane.graphql.types.issues.attachment import (
IssueAttachmentPresignedUrlResponseType,
)
from plane.graphql.permissions.project import ProjectBasePermission
from plane.bgtasks.issue_activities_task import issue_activity
from plane.bgtasks.storage_metadata_task import get_asset_object_metadata
@sync_to_async
def validate_feature_flag(slug: str, user_id: str, feature_key: str) -> bool:
url = f"{settings.FEATURE_FLAG_SERVER_BASE_URL}/api/feature-flags/"
json = {"workspace_slug": slug, "user_id": user_id, "flag_key": feature_key}
headers = {
"content-type": "application/json",
"x-api-key": settings.FEATURE_FLAG_SERVER_AUTH_TOKEN,
}
response = requests.post(url, json=json, headers=headers)
response.raise_for_status()
return response.json().get("value", False)
@sync_to_async
def get_workspace(slug):
try:
return Workspace.objects.get(slug=slug)
except Workspace.DoesNotExist:
return None
@sync_to_async
def create_file_asset(
workspace, project_id, issue_id, attributes, asset_key, size, user_id
) -> FileAssetType:
return FileAsset.objects.create(
workspace=workspace,
project_id=project_id,
issue_id=issue_id,
attributes=attributes,
asset=asset_key,
size=size,
created_by_id=user_id,
entity_type=FileAssetEntityType.ISSUE_ATTACHMENT.value,
)
@strawberry.type
class IssueAttachmentMutation:
@strawberry.mutation(
extensions=[PermissionExtension(permissions=[ProjectBasePermission()])]
)
async def create_issue_attachment(
self,
info: Info,
slug: str,
project: strawberry.ID,
issue: strawberry.ID,
name: str,
type: str,
size: int,
) -> IssueAttachmentPresignedUrlResponseType:
user = info.context.user
# Checking if the file size is within the limit
size_limit = min(size, settings.FILE_SIZE_LIMIT)
if validate_feature_flag(
slug=slug, user_id=str(user.id), feature_key="FILE_SIZE_LIMIT_PRO"
):
size_limit = min(size, settings.PRO_FILE_SIZE_LIMIT)
if type not in settings.ATTACHMENT_MIME_TYPES:
message = "Invalid file type."
error_extensions = {"code": "INVALID_FILE_TYPE", "statusCode": 400}
raise GraphQLError(message, extensions=error_extensions)
# getting the workspace
workspace = await get_workspace(slug=slug)
if workspace is None:
message = "Workspace not found."
error_extensions = {"code": "WORKSPACE_NOT_FOUND", "statusCode": 404}
raise GraphQLError(message, extensions=error_extensions)
# asset key
asset_key = f"{workspace.id}/{uuid.uuid4().hex}-{name}"
# creating a file asset
attachment = await create_file_asset(
workspace=workspace,
project_id=project,
issue_id=issue,
attributes={"name": name, "type": type, "size": size_limit},
asset_key=asset_key,
size=size_limit,
user_id=user.id,
)
# Get the presigned URL
storage = S3Storage(request=info.context["request"])
# Generate a presigned URL to share an S3 object
presigned_url_data = await sync_to_async(storage.generate_presigned_post)(
object_name=asset_key, file_type=type, file_size=size_limit
)
# Return the presigned URL
return IssueAttachmentPresignedUrlResponseType(
upload_data=presigned_url_data,
attachment_id=str(attachment.id),
asset_url=attachment.asset_url,
)
@strawberry.mutation(
extensions=[PermissionExtension(permissions=[ProjectBasePermission()])]
)
async def update_issue_attachment(
self,
info: Info,
slug: str,
project: strawberry.ID,
issue: strawberry.ID,
attachment: strawberry.ID,
attributes: Optional[JSON] = None,
) -> FileAssetType:
user = info.context.user
issue_attachment = await sync_to_async(FileAsset.objects.get)(
workspace__slug=slug,
project_id=project,
entity_type=FileAssetEntityType.ISSUE_ATTACHMENT.value,
issue=issue,
id=attachment,
)
if not issue_attachment.is_uploaded:
issue_activity.delay(
type="attachment.activity.created",
requested_data=None,
actor_id=str(info.context.user.id),
issue_id=str(issue),
project_id=str(project),
current_instance=json.dumps({"id": str(attachment)}),
epoch=int(timezone.now().timestamp()),
notification=True,
origin=info.context.request.META.get("HTTP_ORIGIN"),
)
issue_attachment.is_uploaded = True
issue_attachment.created_by = user
# get the storage metadata
if not issue_attachment.storage_metadata:
get_asset_object_metadata.delay(asset_id=str(attachment))
# update the file asset attributes
if attributes is not None:
issue_attachment.attributes = attributes
else:
issue_attachment.attributes = issue_attachment.attributes
# save the file asset
await sync_to_async(issue_attachment.save)(
update_fields=["is_uploaded", "created_by", "attributes"]
)
return issue_attachment
@strawberry.mutation(
extensions=[PermissionExtension(permissions=[ProjectBasePermission()])]
)
async def delete_issue_attachment(
self,
info: Info,
slug: str,
project: strawberry.ID,
issue: strawberry.ID,
attachment: strawberry.ID,
) -> bool:
issue_attachment = await sync_to_async(FileAsset.objects.get)(
workspace__slug=slug,
project_id=project,
entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
issue=issue,
id=attachment,
)
if not issue_attachment:
message = "Attachment not found."
error_extensions = {"code": "ATTACHMENT_NOT_FOUND", "statusCode": 400}
raise GraphQLError(message, extensions=error_extensions)
issue_attachment.is_deleted = True
issue_attachment.deleted_at = timezone.now()
# save the file asset
await sync_to_async(issue_attachment.save)(
update_fields=["is_deleted", "deleted_at"]
)
issue_activity.delay(
type="attachment.activity.deleted",
requested_data=None,
actor_id=str(info.context.user.id),
issue_id=str(issue),
project_id=str(project),
current_instance=None,
epoch=int(timezone.now().timestamp()),
notification=True,
origin=info.context.request.META.get("HTTP_ORIGIN"),
)
return True

View File

@@ -7,10 +7,11 @@ from asgiref.sync import sync_to_async
# Strawberry Imports
from strawberry.types import Info
from strawberry.permission import PermissionExtension
from strawberry.exceptions import GraphQLError
# Module Imports
from plane.db.models import FileAsset
from plane.graphql.types.issues.attachment import IssueAttachmentType
from plane.graphql.types.asset import FileAssetType
from plane.graphql.permissions.project import ProjectBasePermission
@@ -21,14 +22,41 @@ class IssueAttachmentQuery:
)
async def issue_attachment(
self, info: Info, slug: str, project: strawberry.ID, issue: strawberry.ID
) -> list[IssueAttachmentType]:
) -> list[FileAssetType]:
issue_attachments = await sync_to_async(list)(
FileAsset.objects.filter(
workspace__slug=slug,
project_id=project,
entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
issue=issue,
is_uploaded=True,
)
)
return issue_attachments
@strawberry.field(
extensions=[PermissionExtension(permissions=[ProjectBasePermission()])]
)
async def issue_attachment_detail(
self,
info: Info,
slug: str,
project: strawberry.ID,
issue: strawberry.ID,
attachment: strawberry.ID,
) -> FileAssetType:
issue_attachment = await sync_to_async(FileAsset.objects.get)(
workspace__slug=slug,
project_id=project,
entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
issue=issue,
id=attachment,
)
if not issue_attachment.is_uploaded:
message = "The attachment is not uploaded."
error_extensions = {"code": "ATTACHMENT_NOT_UPLOADED", "statusCode": 400}
raise GraphQLError(message, extensions=error_extensions)
return issue_attachment

View File

@@ -64,7 +64,6 @@ from .mutations.project import (
from .mutations.issue import (
IssueMutation,
IssueUserPropertyMutation,
IssueAttachmentMutation,
IssueSubscriptionMutation,
)
from .mutations.notification import NotificationMutation
@@ -88,6 +87,7 @@ from .mutations.issues import (
IssueModuleMutation,
IssueCycleMutation,
IssueLinkMutation,
IssueAttachmentMutation,
)
from .mutations.device import DeviceInformationMutation
from .mutations.asset import (

View File

@@ -1,4 +1,9 @@
from .base import FileAssetType, AssetPresignedUrlResponseType, UserAssetFileEnumType
from .base import (
FileAssetAssetType,
FileAssetEntityType,
FileAssetType,
AssetPresignedUrlResponseType,
)
from .user import UserAssetEnumType
from .workspace import WorkspaceAssetEnumType
from .project import ProjectAssetEnumType

View File

@@ -1,6 +1,7 @@
# Python imports
from typing import Optional
from enum import Enum
from datetime import datetime
# Strawberry imports
import strawberry
@@ -11,35 +12,8 @@ from strawberry.scalars import JSON
from plane.db.models import FileAsset
@strawberry_django.type(FileAsset)
class FileAssetType:
id: strawberry.ID
attributes: JSON
asset: str
entity_type: str
size: float
is_uploaded: bool
storage_metadata: Optional[JSON]
user: strawberry.ID
workspace: strawberry.ID
project: Optional[strawberry.ID]
issue: Optional[strawberry.ID]
comment: Optional[strawberry.ID]
page: Optional[strawberry.ID]
draft_issue: Optional[strawberry.ID]
is_archived: bool
is_deleted: bool
deleted_at: Optional[str]
created_by: Optional[strawberry.ID]
updated_by: Optional[strawberry.ID]
created_at: str
updated_at: str
# model properties
asset_url: Optional[str]
@strawberry.enum
class UserAssetFileEnumType(Enum):
class FileAssetAssetType(Enum):
IMAGE_JPEG = "image/jpeg"
IMAGE_PNG = "image/png"
IMAGE_WEBP = "image/webp"
@@ -50,6 +24,66 @@ class UserAssetFileEnumType(Enum):
return self.value
@strawberry.enum
class FileAssetEntityType(Enum):
USER_COVER = "USER_COVER"
USER_AVATAR = "USER_AVATAR"
WORKSPACE_LOGO = "WORKSPACE_LOGO"
PROJECT_COVER = "PROJECT_COVER"
PROJECT_DESCRIPTION = "PROJECT_DESCRIPTION"
PROJECT_ATTACHMENT = "PROJECT_ATTACHMENT"
PAGE_DESCRIPTION = "PAGE_DESCRIPTION"
ISSUE_DESCRIPTION = "ISSUE_DESCRIPTION"
DRAFT_ISSUE_DESCRIPTION = "DRAFT_ISSUE_DESCRIPTION"
COMMENT_DESCRIPTION = "COMMENT_DESCRIPTION"
ISSUE_ATTACHMENT = "ISSUE_ATTACHMENT"
DRAFT_ISSUE_ATTACHMENT = "DRAFT_ISSUE_ATTACHMENT"
INITIATIVE_DESCRIPTION = "INITIATIVE_DESCRIPTION"
INITIATIVE_ATTACHMENT = "INITIATIVE_ATTACHMENT"
INITIATIVE_COMMENT_DESCRIPTION = "INITIATIVE_COMMENT_DESCRIPTION"
TEAM_SPACE_DESCRIPTION = "TEAM_SPACE_DESCRIPTION"
TEAM_SPACE_COMMENT_DESCRIPTION = "TEAM_SPACE_COMMENT_DESCRIPTION"
def __str__(self):
return self.value
@strawberry_django.type(FileAsset)
class FileAssetType:
id: strawberry.ID
attributes: JSON
asset: str
size: float
entity_type: FileAssetEntityType
entity_identifier: Optional[strawberry.ID]
storage_metadata: Optional[JSON]
user: strawberry.ID
workspace: strawberry.ID
draft_issue: Optional[strawberry.ID]
project: Optional[strawberry.ID]
issue: Optional[strawberry.ID]
comment: Optional[strawberry.ID]
page: Optional[strawberry.ID]
is_uploaded: bool
is_archived: bool
is_deleted: bool
deleted_at: Optional[str]
external_id: Optional[strawberry.ID]
external_source: Optional[str]
created_by: Optional[strawberry.ID]
updated_by: Optional[strawberry.ID]
created_at: Optional[datetime]
updated_at: Optional[datetime]
# model properties
asset_url: Optional[str]
@strawberry.type
class AssetPresignedUrlResponseType:
upload_data: JSON

View File

@@ -4,15 +4,19 @@ from enum import Enum
# Strawberry imports
import strawberry
# module imports
from plane.graphql.types.asset.base import FileAssetEntityType
@strawberry.enum
class ProjectAssetEnumType(Enum):
PROJECT_COVER = "PROJECT_COVER"
ISSUE_ATTACHMENT = "ISSUE_ATTACHMENT"
ISSUE_DESCRIPTION = "ISSUE_DESCRIPTION"
PAGE_DESCRIPTION = "PAGE_DESCRIPTION"
COMMENT_DESCRIPTION = "COMMENT_DESCRIPTION"
DRAFT_ISSUE_DESCRIPTION = "DRAFT_ISSUE_DESCRIPTION"
PROJECT_COVER = FileAssetEntityType.PROJECT_COVER.value
PAGE_DESCRIPTION = FileAssetEntityType.PAGE_DESCRIPTION.value
ISSUE_DESCRIPTION = FileAssetEntityType.ISSUE_DESCRIPTION.value
DRAFT_ISSUE_DESCRIPTION = FileAssetEntityType.DRAFT_ISSUE_DESCRIPTION.value
COMMENT_DESCRIPTION = FileAssetEntityType.COMMENT_DESCRIPTION.value
ISSUE_ATTACHMENT = FileAssetEntityType.ISSUE_ATTACHMENT.value
DRAFT_ISSUE_ATTACHMENT = FileAssetEntityType.DRAFT_ISSUE_ATTACHMENT.value
def __str__(self):
return self.value

View File

@@ -4,11 +4,14 @@ from enum import Enum
# Strawberry imports
import strawberry
# module imports
from plane.graphql.types.asset.base import FileAssetEntityType
@strawberry.enum
class UserAssetEnumType(Enum):
USER_AVATAR = "USER_AVATAR"
USER_COVER = "USER_COVER"
USER_AVATAR = FileAssetEntityType.USER_AVATAR.value
USER_COVER = FileAssetEntityType.USER_COVER.value
def __str__(self):
return self.value

View File

@@ -4,12 +4,15 @@ from enum import Enum
# Strawberry imports
import strawberry
# module imports
from plane.graphql.types.asset.base import FileAssetEntityType
@strawberry.enum
class WorkspaceAssetEnumType(Enum):
WORKSPACE_LOGO = "WORKSPACE_LOGO"
PROJECT_COVER = "PROJECT_COVER"
PAGE_DESCRIPTION = "PAGE_DESCRIPTION"
WORKSPACE_LOGO = FileAssetEntityType.WORKSPACE_LOGO.value
PROJECT_COVER = FileAssetEntityType.PROJECT_COVER.value
PAGE_DESCRIPTION = FileAssetEntityType.PAGE_DESCRIPTION.value
def __str__(self):
return self.value

View File

@@ -3,62 +3,11 @@ from typing import Optional
# Strawberry imports
import strawberry
import strawberry_django
from strawberry.scalars import JSON
# Module Imports
from plane.db.models import FileAsset
@strawberry_django.type(FileAsset)
class IssueAttachmentType:
id: strawberry.ID
user: strawberry.ID
workspace: strawberry.ID
draft_issue: strawberry.ID
project: strawberry.ID
issue: strawberry.ID
comment: strawberry.ID
page: strawberry.ID
attributes: Optional[JSON]
asset: str
entity_type: str
is_deleted: bool
is_archived: bool
size: float
is_uploaded: bool
storage_metadata: Optional[JSON]
@strawberry.type
class IssueAttachmentPresignedUrlResponseType:
upload_data: JSON
attachment_id: str
asset_url: Optional[str]
external_id: strawberry.ID
external_source: str
@strawberry.field
def user(self):
return self.user_id
@strawberry.field
def workspace(self):
return self.workspace_id
@strawberry.field
def draft_issue(self):
return self.draft_issue_id
@strawberry.field
def project(self):
return self.project_id
@strawberry.field
def issue(self):
return self.external_id
@strawberry.field
def comment(self):
return self.comment_id
@strawberry.field
def page(self):
return self.page_id