From e1f0da5e6ca64ec0db734f8f52556fd07fb352dc Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Fri, 2 Feb 2024 14:48:50 +0530 Subject: [PATCH] dev: update file image urls to backend apis --- apiserver/plane/app/serializers/__init__.py | 1 - apiserver/plane/app/serializers/user.py | 11 +- apiserver/plane/app/urls/asset.py | 1 - apiserver/plane/app/urls/project.py | 11 ++ apiserver/plane/app/urls/user.py | 30 ++-- apiserver/plane/app/urls/workspace.py | 11 ++ apiserver/plane/app/views/__init__.py | 5 +- apiserver/plane/app/views/project.py | 53 ++++++- apiserver/plane/app/views/user.py | 94 +++++++++--- apiserver/plane/app/views/workspace.py | 48 ++++++- .../db/migrations/0059_auto_20240131_1334.py | 135 +++++------------- .../db/migrations/0060_fileasset_size.py | 85 +++++++++++ apiserver/plane/db/models/__init__.py | 2 +- apiserver/plane/db/models/asset.py | 5 + apiserver/plane/db/models/user.py | 36 ----- 15 files changed, 344 insertions(+), 184 deletions(-) create mode 100644 apiserver/plane/db/migrations/0060_fileasset_size.py diff --git a/apiserver/plane/app/serializers/__init__.py b/apiserver/plane/app/serializers/__init__.py index 07d22f423..0d72f9192 100644 --- a/apiserver/plane/app/serializers/__init__.py +++ b/apiserver/plane/app/serializers/__init__.py @@ -7,7 +7,6 @@ from .user import ( UserAdminLiteSerializer, UserMeSerializer, UserMeSettingsSerializer, - UserAssetSerializer, ) from .workspace import ( WorkSpaceSerializer, diff --git a/apiserver/plane/app/serializers/user.py b/apiserver/plane/app/serializers/user.py index 571560131..de62b2655 100644 --- a/apiserver/plane/app/serializers/user.py +++ b/apiserver/plane/app/serializers/user.py @@ -3,7 +3,7 @@ from rest_framework import serializers # Module import from .base import BaseSerializer -from plane.db.models import User, Workspace, WorkspaceMemberInvite, UserAsset +from plane.db.models import User, Workspace, WorkspaceMemberInvite class UserSerializer(BaseSerializer): @@ -196,12 +196,3 @@ class ResetPasswordSerializer(serializers.Serializer): """ new_password = serializers.CharField(required=True, min_length=8) - - -class UserAssetSerializer(BaseSerializer): - class Meta: - model = UserAsset - fields = "__all__" - read_only_fields = [ - "user", - ] diff --git a/apiserver/plane/app/urls/asset.py b/apiserver/plane/app/urls/asset.py index 77461313c..288cc84fd 100644 --- a/apiserver/plane/app/urls/asset.py +++ b/apiserver/plane/app/urls/asset.py @@ -3,7 +3,6 @@ from django.urls import path from plane.app.views import ( FileAssetEndpoint, - UserAssetsEndpoint, FileAssetViewSet, ) diff --git a/apiserver/plane/app/urls/project.py b/apiserver/plane/app/urls/project.py index f8ecac4c0..1f26c6759 100644 --- a/apiserver/plane/app/urls/project.py +++ b/apiserver/plane/app/urls/project.py @@ -14,6 +14,7 @@ from plane.app.views import ( ProjectPublicCoverImagesEndpoint, ProjectDeployBoardViewSet, UserProjectRolesEndpoint, + ProjectCoverImageEndpoint, ) @@ -175,4 +176,14 @@ urlpatterns = [ ), name="project-deploy-board", ), + path( + "workspaces//projects//cover-image///", + ProjectCoverImageEndpoint.as_view(), + name="project-cover-image", + ), + path( + "workspaces//projects//cover-image/", + ProjectCoverImageEndpoint.as_view(), + name="project-cover-image", + ), ] diff --git a/apiserver/plane/app/urls/user.py b/apiserver/plane/app/urls/user.py index 789b20188..2357fddaa 100644 --- a/apiserver/plane/app/urls/user.py +++ b/apiserver/plane/app/urls/user.py @@ -8,7 +8,6 @@ from plane.app.views import ( UserActivityEndpoint, ChangePasswordEndpoint, SetUserPasswordEndpoint, - UserAssetsEndpoint, ## End User ## Workspaces UserWorkSpacesEndpoint, @@ -16,6 +15,10 @@ from plane.app.views import ( UserIssueCompletedGraphEndpoint, UserWorkspaceDashboardEndpoint, ## End Workspaces + # Asset Endpoints ## + UserAvatarEndpoint, + UserCoverImageEndpoint, + ## End Asset Endpoint ## ) urlpatterns = [ @@ -96,15 +99,26 @@ urlpatterns = [ SetUserPasswordEndpoint.as_view(), name="set-password", ), + # User Assets path( - "users/assets/", - UserAssetsEndpoint.as_view(), - name="user-assets", + "users/avatar/", + UserAvatarEndpoint.as_view(), + name="user-avatar", ), path( - "users/assets//", - UserAssetsEndpoint.as_view(), - name="user-assets", + "users/avatar//", + UserAvatarEndpoint.as_view(), + name="user-avatar", ), - ## End User Graph + path( + "users/cover-image/", + UserCoverImageEndpoint.as_view(), + name="user-avatar", + ), + path( + "users/cover-image//", + UserCoverImageEndpoint.as_view(), + name="user-avatar", + ), + ## User Assets ] diff --git a/apiserver/plane/app/urls/workspace.py b/apiserver/plane/app/urls/workspace.py index 7e64e586a..44a2c401a 100644 --- a/apiserver/plane/app/urls/workspace.py +++ b/apiserver/plane/app/urls/workspace.py @@ -22,6 +22,7 @@ from plane.app.views import ( WorkspaceUserPropertiesEndpoint, WorkspaceStatesEndpoint, WorkspaceEstimatesEndpoint, + WorkspaceLogoEndpoint, ) @@ -219,4 +220,14 @@ urlpatterns = [ WorkspaceEstimatesEndpoint.as_view(), name="workspace-estimate", ), + path( + "workspaces//logo/", + WorkspaceLogoEndpoint.as_view(), + name="workspace-logo", + ), + path( + "workspaces//logo///", + WorkspaceLogoEndpoint.as_view(), + name="workspace-logo", + ), ] diff --git a/apiserver/plane/app/views/__init__.py b/apiserver/plane/app/views/__init__.py index 6adffdb83..3061b8300 100644 --- a/apiserver/plane/app/views/__init__.py +++ b/apiserver/plane/app/views/__init__.py @@ -12,13 +12,15 @@ from .project import ( ProjectPublicCoverImagesEndpoint, ProjectDeployBoardViewSet, UserProjectRolesEndpoint, + ProjectCoverImageEndpoint, ) from .user import ( UserEndpoint, UpdateUserOnBoardedEndpoint, UpdateUserTourCompletedEndpoint, UserActivityEndpoint, - UserAssetsEndpoint, + UserAvatarEndpoint, + UserCoverImageEndpoint, ) from .oauth import OauthEndpoint @@ -50,6 +52,7 @@ from .workspace import ( WorkspaceUserPropertiesEndpoint, WorkspaceStatesEndpoint, WorkspaceEstimatesEndpoint, + WorkspaceLogoEndpoint, ) from .state import StateViewSet from .view import ( diff --git a/apiserver/plane/app/views/project.py b/apiserver/plane/app/views/project.py index 5d2f95673..882a189dc 100644 --- a/apiserver/plane/app/views/project.py +++ b/apiserver/plane/app/views/project.py @@ -18,12 +18,13 @@ from django.db.models import ( from django.core.validators import validate_email from django.conf import settings from django.utils import timezone - +from django.http import HttpResponseRedirect # Third Party imports from rest_framework.response import Response from rest_framework import status from rest_framework import serializers -from rest_framework.permissions import AllowAny +from rest_framework.permissions import AllowAny, IsAuthenticated +from rest_framework.parsers import MultiPartParser, FormParser, JSONParser # Module imports from .base import BaseViewSet, BaseAPIView, WebhookMixin @@ -37,6 +38,8 @@ from plane.app.serializers import ( ProjectDeployBoardSerializer, ProjectMemberAdminSerializer, ProjectMemberRoleSerializer, + ProjectLiteSerializer, + FileAssetSerializer, ) from plane.app.permissions import ( @@ -62,10 +65,10 @@ from plane.db.models import ( Inbox, ProjectDeployBoard, IssueProperty, + FileAsset, ) -from plane.bgtasks.project_invitation_task import project_invitation - +from plane.utils.presigned_url_generator import generate_download_presigned_url class ProjectViewSet(WebhookMixin, BaseViewSet): serializer_class = ProjectListSerializer @@ -1138,3 +1141,45 @@ class UserProjectRolesEndpoint(BaseAPIView): for member in project_members } return Response(project_members, status=status.HTTP_200_OK) + + +class ProjectCoverImageEndpoint(BaseAPIView): + + parser_classes = ( + MultiPartParser, + FormParser, + JSONParser, + ) + + def get_permissions(self): + if self.request.method == "POST" or self.request.method == "DELETE": + return [ + IsAuthenticated(), + ] + return [ + AllowAny(), + ] + + def get(self, request, slug, project_id, workspace_id, cover_image_key): + key = f"{workspace_id}/{cover_image_key}" + url = generate_download_presigned_url(key) + return HttpResponseRedirect(url) + + def post(self, request, slug, project_id): + serializer = FileAssetSerializer(data=request.data) + workspace = Workspace.objects.get(slug=slug) + if serializer.is_valid(): + serializer.save(workspace=workspace) + project = Project.objects.get(pk=project_id) + project.cover_image = f"/api/workspaces/{slug}/projects/{project_id}/cover-image/{serializer.data['asset']}/" + project.save() + project_serializer = ProjectLiteSerializer(project) + return Response(project_serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def delete(self, request, project_id, workspace_id, cover_image_key): + key = f"{workspace_id}/{cover_image_key}" + file_asset = FileAsset.objects.get(asset=key) + file_asset.is_deleted = True + file_asset.save() + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/app/views/user.py b/apiserver/plane/app/views/user.py index bd0218ca6..ff8266753 100644 --- a/apiserver/plane/app/views/user.py +++ b/apiserver/plane/app/views/user.py @@ -2,7 +2,7 @@ import requests # Django imports -from django.http import StreamingHttpResponse +from django.http import HttpResponseRedirect from django.conf import settings # Third party imports @@ -10,6 +10,7 @@ import boto3 from rest_framework.response import Response from rest_framework import status from rest_framework.parsers import MultiPartParser, FormParser, JSONParser +from rest_framework.permissions import AllowAny, IsAuthenticated # Module imports from plane.app.serializers import ( @@ -17,14 +18,14 @@ from plane.app.serializers import ( IssueActivitySerializer, UserMeSerializer, UserMeSettingsSerializer, - UserAssetSerializer, + FileAssetSerializer, ) from plane.app.views.base import BaseViewSet, BaseAPIView -from plane.db.models import User, IssueActivity, WorkspaceMember, ProjectMember, UserAsset +from plane.db.models import User, IssueActivity, WorkspaceMember, ProjectMember, FileAsset from plane.license.models import Instance, InstanceAdmin from plane.utils.paginator import BasePaginator -from plane.utils.file_stream import get_file_streams +from plane.utils.presigned_url_generator import generate_download_presigned_url from django.db.models import Q, F, Count, Case, When, IntegerField @@ -188,25 +189,82 @@ class UserActivityEndpoint(BaseAPIView, BasePaginator): ) -class UserAssetsEndpoint(BaseAPIView): - parser_classes = (MultiPartParser, FormParser) +class UserAvatarEndpoint(BaseAPIView): + + parser_classes = ( + MultiPartParser, + FormParser, + JSONParser, + ) + + def get_permissions(self): + if self.request.method == "POST" or self.request.method == "DELETE": + return [ + IsAuthenticated(), + ] + return [ + AllowAny(), + ] + + + def get(self, request, avatar_key): + url = generate_download_presigned_url(avatar_key) + return HttpResponseRedirect(url) def post(self, request): - serializer = UserAssetSerializer(data=request.data) + serializer = FileAssetSerializer(data=request.data) if serializer.is_valid(): - serializer.save(user=request.user) - return Response(serializer.data, status=status.HTTP_201_CREATED) + # Get the workspace + serializer.save() + user = request.user + user.avatar = "/api/users/avatar/" + serializer.data["asset"] + "/" + user.save() + user_serializer = UserMeSerializer(user) + return Response(user_serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - def delete(self, request, asset_key): - user_asset = UserAsset.objects.get( - asset=asset_key, created_by=request.user - ) - user_asset.is_deleted = True - user_asset.save() + def delete(self, request, avatar_key): + file_asset = FileAsset.objects.get(asset=avatar_key) + file_asset.is_deleted = True + file_asset.save() return Response(status=status.HTTP_204_NO_CONTENT) - def get(self, request, key): - response = get_file_streams(key, key) - return response +class UserCoverImageEndpoint(BaseAPIView): + + parser_classes = ( + MultiPartParser, + FormParser, + JSONParser, + ) + + def get_permissions(self): + if self.request.method == "POST" or self.request.method == "DELETE": + return [ + IsAuthenticated(), + ] + return [ + AllowAny(), + ] + + def get(self, request, cover_image_key): + url = generate_download_presigned_url(cover_image_key) + return HttpResponseRedirect(url) + + def post(self, request): + serializer = FileAssetSerializer(data=request.data) + if serializer.is_valid(): + # Get the workspace + serializer.save() + user = request.user + user.avatar = "/api/users/cover-image/" + serializer.data["asset"] + "/" + user.save() + user_serializer = UserMeSerializer(user) + return Response(user_serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def delete(self, request, cover_image_key): + file_asset = FileAsset.objects.get(asset=cover_image_key) + file_asset.is_deleted = True + file_asset.save() + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/app/views/workspace.py b/apiserver/plane/app/views/workspace.py index f4d3dbbb5..a303be622 100644 --- a/apiserver/plane/app/views/workspace.py +++ b/apiserver/plane/app/views/workspace.py @@ -25,11 +25,13 @@ from django.db.models import ( ) from django.db.models.functions import ExtractWeek, Cast, ExtractDay from django.db.models.fields import DateField +from django.http import HttpResponseRedirect # Third party modules from rest_framework import status from rest_framework.response import Response -from rest_framework.permissions import AllowAny +from rest_framework.permissions import AllowAny, IsAuthenticated +from rest_framework.parsers import MultiPartParser, FormParser, JSONParser # Module imports from plane.app.serializers import ( @@ -49,6 +51,7 @@ from plane.app.serializers import ( WorkspaceEstimateSerializer, StateSerializer, LabelSerializer, + FileAssetSerializer, ) from plane.app.views.base import BaseAPIView from . import BaseViewSet @@ -73,6 +76,7 @@ from plane.db.models import ( WorkspaceUserProperties, Estimate, EstimatePoint, + FileAsset, ) from plane.app.permissions import ( WorkSpaceBasePermission, @@ -84,6 +88,7 @@ from plane.app.permissions import ( ) from plane.bgtasks.workspace_invitation_task import workspace_invitation from plane.utils.issue_filters import issue_filters +from plane.utils.presigned_url_generator import generate_download_presigned_url from plane.bgtasks.event_tracking_task import workspace_invite_event @@ -1524,3 +1529,44 @@ class WorkspaceUserPropertiesEndpoint(BaseAPIView): ) serializer = WorkspaceUserPropertiesSerializer(workspace_properties) return Response(serializer.data, status=status.HTTP_200_OK) + + +class WorkspaceLogoEndpoint(BaseAPIView): + + parser_classes = ( + MultiPartParser, + FormParser, + JSONParser, + ) + + def get_permissions(self): + if self.request.method == "POST" or self.request.method == "DELETE": + return [ + IsAuthenticated(), + ] + return [ + AllowAny(), + ] + + def get(self, request, slug, workspace_id, logo_key): + key = f"{workspace_id}/{logo_key}" + url = generate_download_presigned_url(key) + return HttpResponseRedirect(url) + + def post(self, request, slug): + serializer = FileAssetSerializer(data=request.data) + workspace = Workspace.objects.get(slug=slug) + if serializer.is_valid(): + serializer.save(workspace=workspace) + workspace.logo = f"/api/workspaces/{slug}/logo/{serializer.data['asset']}/" + workspace.save() + workspace_serializer = WorkSpaceSerializer(workspace) + return Response(workspace_serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + def delete(self, request, slug, workspace_id, logo_key): + key = f"{workspace_id}/{logo_key}" + file_asset = FileAsset.objects.get(asset=key) + file_asset.is_deleted = True + file_asset.save() + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apiserver/plane/db/migrations/0059_auto_20240131_1334.py b/apiserver/plane/db/migrations/0059_auto_20240131_1334.py index 3058c1776..61e9c03bf 100644 --- a/apiserver/plane/db/migrations/0059_auto_20240131_1334.py +++ b/apiserver/plane/db/migrations/0059_auto_20240131_1334.py @@ -9,49 +9,52 @@ import plane.db.models.asset def update_urls(apps, schema_editor): # Check if the app is using minio or s3 if settings.USE_MINIO: - prefix = ( + prefix1 = ( f"{settings.AWS_S3_URL_PROTOCOL}//{settings.AWS_S3_CUSTOM_DOMAIN}/" ) - prefix2 = prefix + prefix2 = prefix1 else: - prefix = f"https://{settings.AWS_STORAGE_BUCKET_NAME}.s3.{settings.AWS_REGION}.amazonaws.com/" + prefix1 = f"https://{settings.AWS_STORAGE_BUCKET_NAME}.s3.{settings.AWS_REGION}.amazonaws.com/" prefix2 = ( f"https://{settings.AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com/" ) User = apps.get_model("db", "User") - UserAsset = apps.get_model("db", "UserAsset") bulk_users = [] - bulk_user_assets = [] - for user in User.objects.all(): - if user.avatar and ( - user.avatar.startswith(prefix) or user.avatar.startswith(prefix2) - ): - avatar = user.avatar[len(prefix) :] - user.avatar = avatar - bulk_user_assets.append( - UserAsset( - user=user, - asset=avatar, - ) - ) - if user.cover_image and ( - user.cover_image.startswith(prefix) - or user.cover_image.startswith(prefix2) - ): - cover_image = user.cover_image[len(prefix) :] - user.cover_image = cover_image - bulk_user_assets.append( - UserAsset( - user=user, - asset=cover_image, - ) + # Loop through all the users and update the cover image + for user in User.objects.all(): + # prefix 1 + if user.avatar and (user.avatar.startswith(prefix1)): + avatar_key = user.avatar + user.avatar = "/api/users/avatar/" + avatar_key[len(prefix1) :] + "/" + bulk_users.append(user) + + # prefix 2 + if not settings.USE_MINIO and user.avatar and user.avatar.startswith(prefix2): + avatar_key = user.avatar + user.avatar = "/api/users/avatar/" + avatar_key[len(prefix2) :] + "/" + bulk_users.append(user) + + # prefix 1 + if user.cover_image and (user.cover_image.startswith(prefix1)): + cover_image_key = user.cover_image + user.cover_image = ( + "/api/users/cover-image/" + cover_image_key[len(prefix1) :] + "/" ) + bulk_users.append(user) + + # prefix 2 + if not settings.USE_MINIO and user.cover_image and user.cover_image.startswith(prefix2): + cover_image_key = user.cover_image + user.cover_image = ( + "/api/users/cover-image/" + cover_image_key[len(prefix2) :] + "/" + ) + bulk_users.append(user) + User.objects.bulk_update( bulk_users, ["avatar", "cover_image"], batch_size=100 ) - UserAsset.objects.bulk_create(bulk_user_assets, batch_size=100) class Migration(migrations.Migration): @@ -60,80 +63,6 @@ class Migration(migrations.Migration): ] operations = [ - migrations.CreateModel( - name="UserAsset", - fields=[ - ( - "created_at", - models.DateTimeField( - auto_now_add=True, verbose_name="Created At" - ), - ), - ( - "updated_at", - models.DateTimeField( - auto_now=True, verbose_name="Last Modified At" - ), - ), - ( - "id", - models.UUIDField( - db_index=True, - default=uuid.uuid4, - editable=False, - primary_key=True, - serialize=False, - unique=True, - ), - ), - ( - "asset", - models.FileField( - max_length=500, - storage=plane.settings.storage.S3PrivateBucketStorage(), - upload_to=plane.db.models.user.get_upload_path, - validators=[plane.db.models.user.file_size], - ), - ), - ("is_deleted", models.BooleanField(default=False)), - ("size", models.PositiveBigIntegerField(null=True)), - ("attributes", models.JSONField(default=dict)), - ( - "created_by", - models.ForeignKey( - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="%(class)s_created_by", - to=settings.AUTH_USER_MODEL, - verbose_name="Created By", - ), - ), - ( - "updated_by", - models.ForeignKey( - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="%(class)s_updated_by", - to=settings.AUTH_USER_MODEL, - verbose_name="Last Modified By", - ), - ), - ( - "user", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="assets", - to=settings.AUTH_USER_MODEL, - ), - ), - ], - options={ - "verbose_name": "User Asset", - "verbose_name_plural": "User Assets", - "db_table": "user_assets", - "ordering": ("-created_at",), - }, - ), migrations.AlterField( model_name="fileasset", name="asset", diff --git a/apiserver/plane/db/migrations/0060_fileasset_size.py b/apiserver/plane/db/migrations/0060_fileasset_size.py new file mode 100644 index 000000000..e1461eb7d --- /dev/null +++ b/apiserver/plane/db/migrations/0060_fileasset_size.py @@ -0,0 +1,85 @@ +# Generated by Django 4.2.7 on 2024-02-02 07:23 + +from django.db import migrations, models +from django.conf import settings + + +def update_workspace_urls(apps, schema_editor): + # Check if the app is using minio or s3 + if settings.USE_MINIO: + prefix1 = ( + f"{settings.AWS_S3_URL_PROTOCOL}//{settings.AWS_S3_CUSTOM_DOMAIN}/" + ) + prefix2 = prefix1 + else: + prefix1 = f"https://{settings.AWS_STORAGE_BUCKET_NAME}.s3.{settings.AWS_REGION}.amazonaws.com/" + prefix2 = ( + f"https://{settings.AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com/" + ) + + Workspace = apps.get_model("db", "Workspace") + bulk_workspaces = [] + + # Loop through all the users and update the cover image + for workspace in Workspace.objects.all(): + # prefix 1 + if workspace.logo and (workspace.logo.startswith(prefix1)): + logo_key = workspace.logo + workspace.logo = f"/api/workspaces/{workspace.slug}/logo/{logo_key[len(prefix1) :]}/" + bulk_workspaces.append(workspace) + + # prefix 2 + if not settings.USE_MINIO and workspace.logo and (workspace.logo.startswith(prefix2)): + logo_key = workspace.logo + workspace.logo = f"/api/workspaces/{workspace.slug}/logo/{logo_key[len(prefix2) :]}/" + bulk_workspaces.append(workspace) + + Workspace.objects.bulk_update(bulk_workspaces, ["logo"], batch_size=100) + +def update_project_urls(apps, schema_editor): + # Check if the app is using minio or s3 + if settings.USE_MINIO: + prefix1 = ( + f"{settings.AWS_S3_URL_PROTOCOL}//{settings.AWS_S3_CUSTOM_DOMAIN}/" + ) + prefix2 = prefix1 + else: + prefix1 = f"https://{settings.AWS_STORAGE_BUCKET_NAME}.s3.{settings.AWS_REGION}.amazonaws.com/" + prefix2 = ( + f"https://{settings.AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com/" + ) + + Project = apps.get_model("db", "Project") + bulk_projects = [] + + # Loop through all the users and update the cover image + for project in Project.objects.all(): + # prefix 1 + if project.cover_image and (project.cover_image.startswith(prefix1)): + cover_image_key = project.cover_image + project.cover_image = f"/api/workspaces/{project.workspace.slug}/projects/{project.id}/cover-image/{cover_image_key[len(prefix1) :]}/" + bulk_projects.append(project) + + # prefix 2 + if not settings.USE_MINIO and project.cover_image and (project.cover_image.startswith(prefix2)): + cover_image_key = project.cover_image + project.cover_image = f"/api/workspaces/{project.workspace.slug}/projects/{project.id}/cover-image/{cover_image_key[len(prefix2) :]}/" + bulk_projects.append(project) + + Project.objects.bulk_update(bulk_projects, ["cover_image"], batch_size=100) + + +class Migration(migrations.Migration): + dependencies = [ + ("db", "0059_auto_20240131_1334"), + ] + + operations = [ + migrations.AddField( + model_name="fileasset", + name="size", + field=models.PositiveBigIntegerField(null=True), + ), + migrations.RunPython(update_workspace_urls), + migrations.RunPython(update_project_urls), + ] diff --git a/apiserver/plane/db/models/__init__.py b/apiserver/plane/db/models/__init__.py index 9575702aa..d9096bd01 100644 --- a/apiserver/plane/db/models/__init__.py +++ b/apiserver/plane/db/models/__init__.py @@ -1,6 +1,6 @@ from .base import BaseModel -from .user import User, UserAsset +from .user import User from .workspace import ( Workspace, diff --git a/apiserver/plane/db/models/asset.py b/apiserver/plane/db/models/asset.py index b327d83bb..c8908e227 100644 --- a/apiserver/plane/db/models/asset.py +++ b/apiserver/plane/db/models/asset.py @@ -41,6 +41,7 @@ class FileAsset(BaseModel): related_name="assets", ) is_deleted = models.BooleanField(default=False) + size = models.PositiveBigIntegerField(null=True) class Meta: verbose_name = "File Asset" @@ -50,3 +51,7 @@ class FileAsset(BaseModel): def __str__(self): return str(self.asset) + + def save(self, *args, **kwargs): + self.size = self.asset.size + super(FileAsset, self).save(*args, **kwargs) diff --git a/apiserver/plane/db/models/user.py b/apiserver/plane/db/models/user.py index 9ebafe0f9..6a0343d2b 100644 --- a/apiserver/plane/db/models/user.py +++ b/apiserver/plane/db/models/user.py @@ -149,42 +149,6 @@ class User(AbstractBaseUser, PermissionsMixin): super(User, self).save(*args, **kwargs) -def get_upload_path(instance, filename): - return f"user-{uuid.uuid4().hex}" - -def file_size(value): - if value.size > settings.FILE_SIZE_LIMIT: - raise ValidationError("File too large. Size should not exceed 5 MB.") - -class UserAsset(BaseModel): - - user = models.ForeignKey("db.User", on_delete=models.CASCADE, related_name="assets") - asset = models.FileField( - upload_to=get_upload_path, - validators=[ - file_size, - ], - storage=S3PrivateBucketStorage(), - max_length=500, - ) - is_deleted = models.BooleanField(default=False) - size = models.PositiveBigIntegerField(null=True) - attributes = models.JSONField(default=dict) - - def save(self, *args, **kwargs): - self.size = self.asset.size - super(UserAsset, self).save(*args, **kwargs) - - class Meta: - verbose_name = "User Asset" - verbose_name_plural = "User Assets" - db_table = "user_assets" - ordering = ("-created_at",) - - def __str__(self): - return str(self.asset) - - @receiver(post_save, sender=User) def send_welcome_slack(sender, instance, created, **kwargs): try: