dev: update file image urls to backend apis

This commit is contained in:
pablohashescobar 2024-02-02 14:48:50 +05:30
parent 94f445cc08
commit e1f0da5e6c
15 changed files with 344 additions and 184 deletions

View File

@ -7,7 +7,6 @@ from .user import (
UserAdminLiteSerializer,
UserMeSerializer,
UserMeSettingsSerializer,
UserAssetSerializer,
)
from .workspace import (
WorkSpaceSerializer,

View File

@ -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",
]

View File

@ -3,7 +3,6 @@ from django.urls import path
from plane.app.views import (
FileAssetEndpoint,
UserAssetsEndpoint,
FileAssetViewSet,
)

View File

@ -14,6 +14,7 @@ from plane.app.views import (
ProjectPublicCoverImagesEndpoint,
ProjectDeployBoardViewSet,
UserProjectRolesEndpoint,
ProjectCoverImageEndpoint,
)
@ -175,4 +176,14 @@ urlpatterns = [
),
name="project-deploy-board",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/cover-image/<str:workspace_id>/<str:cover_image_key>/",
ProjectCoverImageEndpoint.as_view(),
name="project-cover-image",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/cover-image/",
ProjectCoverImageEndpoint.as_view(),
name="project-cover-image",
),
]

View File

@ -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/<str:key>/",
UserAssetsEndpoint.as_view(),
name="user-assets",
"users/avatar/<str:avatar_key>/",
UserAvatarEndpoint.as_view(),
name="user-avatar",
),
## End User Graph
path(
"users/cover-image/",
UserCoverImageEndpoint.as_view(),
name="user-avatar",
),
path(
"users/cover-image/<str:cover_image_key>/",
UserCoverImageEndpoint.as_view(),
name="user-avatar",
),
## User Assets
]

View File

@ -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/<str:slug>/logo/",
WorkspaceLogoEndpoint.as_view(),
name="workspace-logo",
),
path(
"workspaces/<str:slug>/logo/<str:workspace_id>/<str:logo_key>/",
WorkspaceLogoEndpoint.as_view(),
name="workspace-logo",
),
]

View File

@ -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 (

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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",

View File

@ -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),
]

View File

@ -1,6 +1,6 @@
from .base import BaseModel
from .user import User, UserAsset
from .user import User
from .workspace import (
Workspace,

View File

@ -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)

View File

@ -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: