mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
dev: update user assets with backend streaming
This commit is contained in:
parent
42f307421a
commit
94f445cc08
@ -7,6 +7,7 @@ from .user import (
|
|||||||
UserAdminLiteSerializer,
|
UserAdminLiteSerializer,
|
||||||
UserMeSerializer,
|
UserMeSerializer,
|
||||||
UserMeSettingsSerializer,
|
UserMeSettingsSerializer,
|
||||||
|
UserAssetSerializer,
|
||||||
)
|
)
|
||||||
from .workspace import (
|
from .workspace import (
|
||||||
WorkSpaceSerializer,
|
WorkSpaceSerializer,
|
||||||
|
@ -162,17 +162,10 @@ class DynamicBaseSerializer(BaseSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class BaseFileSerializer(DynamicBaseSerializer):
|
class BaseFileSerializer(DynamicBaseSerializer):
|
||||||
download_url = serializers.SerializerMethodField()
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
abstract = True # Make this serializer abstract
|
abstract = True # Make this serializer abstract
|
||||||
|
|
||||||
def get_download_url(self, obj):
|
|
||||||
if hasattr(obj, "asset") and obj.asset:
|
|
||||||
storage = S3PrivateBucketStorage()
|
|
||||||
return storage.download_url(obj.asset.name)
|
|
||||||
return None
|
|
||||||
|
|
||||||
def to_representation(self, instance):
|
def to_representation(self, instance):
|
||||||
"""
|
"""
|
||||||
Object instance -> Dict of primitive datatypes.
|
Object instance -> Dict of primitive datatypes.
|
||||||
|
@ -3,8 +3,7 @@ from rest_framework import serializers
|
|||||||
|
|
||||||
# Module import
|
# Module import
|
||||||
from .base import BaseSerializer
|
from .base import BaseSerializer
|
||||||
from plane.db.models import User, Workspace, WorkspaceMemberInvite
|
from plane.db.models import User, Workspace, WorkspaceMemberInvite, UserAsset
|
||||||
from plane.license.models import InstanceAdmin, Instance
|
|
||||||
|
|
||||||
|
|
||||||
class UserSerializer(BaseSerializer):
|
class UserSerializer(BaseSerializer):
|
||||||
@ -197,3 +196,12 @@ class ResetPasswordSerializer(serializers.Serializer):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
new_password = serializers.CharField(required=True, min_length=8)
|
new_password = serializers.CharField(required=True, min_length=8)
|
||||||
|
|
||||||
|
|
||||||
|
class UserAssetSerializer(BaseSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = UserAsset
|
||||||
|
fields = "__all__"
|
||||||
|
read_only_fields = [
|
||||||
|
"user",
|
||||||
|
]
|
||||||
|
@ -19,16 +19,6 @@ urlpatterns = [
|
|||||||
FileAssetEndpoint.as_view(),
|
FileAssetEndpoint.as_view(),
|
||||||
name="file-assets",
|
name="file-assets",
|
||||||
),
|
),
|
||||||
path(
|
|
||||||
"users/file-assets/",
|
|
||||||
UserAssetsEndpoint.as_view(),
|
|
||||||
name="user-file-assets",
|
|
||||||
),
|
|
||||||
path(
|
|
||||||
"users/file-assets/<str:asset_key>/",
|
|
||||||
UserAssetsEndpoint.as_view(),
|
|
||||||
name="user-file-assets",
|
|
||||||
),
|
|
||||||
path(
|
path(
|
||||||
"workspaces/file-assets/<uuid:workspace_id>/<str:asset_key>/restore/",
|
"workspaces/file-assets/<uuid:workspace_id>/<str:asset_key>/restore/",
|
||||||
FileAssetViewSet.as_view(
|
FileAssetViewSet.as_view(
|
||||||
|
@ -8,6 +8,7 @@ from plane.app.views import (
|
|||||||
UserActivityEndpoint,
|
UserActivityEndpoint,
|
||||||
ChangePasswordEndpoint,
|
ChangePasswordEndpoint,
|
||||||
SetUserPasswordEndpoint,
|
SetUserPasswordEndpoint,
|
||||||
|
UserAssetsEndpoint,
|
||||||
## End User
|
## End User
|
||||||
## Workspaces
|
## Workspaces
|
||||||
UserWorkSpacesEndpoint,
|
UserWorkSpacesEndpoint,
|
||||||
@ -95,5 +96,15 @@ urlpatterns = [
|
|||||||
SetUserPasswordEndpoint.as_view(),
|
SetUserPasswordEndpoint.as_view(),
|
||||||
name="set-password",
|
name="set-password",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"users/assets/",
|
||||||
|
UserAssetsEndpoint.as_view(),
|
||||||
|
name="user-assets",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"users/assets/<str:key>/",
|
||||||
|
UserAssetsEndpoint.as_view(),
|
||||||
|
name="user-assets",
|
||||||
|
),
|
||||||
## End User Graph
|
## End User Graph
|
||||||
]
|
]
|
||||||
|
@ -18,6 +18,7 @@ from .user import (
|
|||||||
UpdateUserOnBoardedEndpoint,
|
UpdateUserOnBoardedEndpoint,
|
||||||
UpdateUserTourCompletedEndpoint,
|
UpdateUserTourCompletedEndpoint,
|
||||||
UserActivityEndpoint,
|
UserActivityEndpoint,
|
||||||
|
UserAssetsEndpoint,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .oauth import OauthEndpoint
|
from .oauth import OauthEndpoint
|
||||||
@ -65,7 +66,7 @@ from .cycle import (
|
|||||||
TransferCycleIssueEndpoint,
|
TransferCycleIssueEndpoint,
|
||||||
CycleUserPropertiesEndpoint,
|
CycleUserPropertiesEndpoint,
|
||||||
)
|
)
|
||||||
from .asset import FileAssetEndpoint, UserAssetsEndpoint, FileAssetViewSet
|
from .asset import FileAssetEndpoint, FileAssetViewSet
|
||||||
from .issue import (
|
from .issue import (
|
||||||
IssueViewSet,
|
IssueViewSet,
|
||||||
WorkSpaceIssuesEndpoint,
|
WorkSpaceIssuesEndpoint,
|
||||||
@ -180,7 +181,4 @@ from .webhook import (
|
|||||||
WebhookSecretRegenerateEndpoint,
|
WebhookSecretRegenerateEndpoint,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .dashboard import (
|
from .dashboard import DashboardEndpoint, WidgetsEndpoint
|
||||||
DashboardEndpoint,
|
|
||||||
WidgetsEndpoint
|
|
||||||
)
|
|
||||||
|
@ -61,40 +61,3 @@ class FileAssetViewSet(BaseViewSet):
|
|||||||
file_asset.is_deleted = False
|
file_asset.is_deleted = False
|
||||||
file_asset.save()
|
file_asset.save()
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
|
||||||
class UserAssetsEndpoint(BaseAPIView):
|
|
||||||
parser_classes = (MultiPartParser, FormParser)
|
|
||||||
|
|
||||||
def get(self, request, asset_key):
|
|
||||||
files = FileAsset.objects.filter(
|
|
||||||
asset=asset_key, created_by=request.user
|
|
||||||
)
|
|
||||||
if files.exists():
|
|
||||||
serializer = FileAssetSerializer(
|
|
||||||
files, context={"request": request}
|
|
||||||
)
|
|
||||||
return Response(
|
|
||||||
{"data": serializer.data, "status": True},
|
|
||||||
status=status.HTTP_200_OK,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
return Response(
|
|
||||||
{"error": "Asset key does not exist", "status": False},
|
|
||||||
status=status.HTTP_200_OK,
|
|
||||||
)
|
|
||||||
|
|
||||||
def post(self, request):
|
|
||||||
serializer = FileAssetSerializer(data=request.data)
|
|
||||||
if serializer.is_valid():
|
|
||||||
serializer.save()
|
|
||||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
|
|
||||||
def delete(self, request, asset_key):
|
|
||||||
file_asset = FileAsset.objects.get(
|
|
||||||
asset=asset_key, created_by=request.user
|
|
||||||
)
|
|
||||||
file_asset.is_deleted = True
|
|
||||||
file_asset.save()
|
|
||||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
|
||||||
|
@ -1,7 +1,15 @@
|
|||||||
|
# Python imports
|
||||||
|
import requests
|
||||||
|
|
||||||
|
# Django imports
|
||||||
|
from django.http import StreamingHttpResponse
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
# Third party imports
|
# Third party imports
|
||||||
|
import boto3
|
||||||
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, JSONParser
|
||||||
|
|
||||||
# Module imports
|
# Module imports
|
||||||
from plane.app.serializers import (
|
from plane.app.serializers import (
|
||||||
@ -9,13 +17,14 @@ from plane.app.serializers import (
|
|||||||
IssueActivitySerializer,
|
IssueActivitySerializer,
|
||||||
UserMeSerializer,
|
UserMeSerializer,
|
||||||
UserMeSettingsSerializer,
|
UserMeSettingsSerializer,
|
||||||
|
UserAssetSerializer,
|
||||||
)
|
)
|
||||||
|
|
||||||
from plane.app.views.base import BaseViewSet, BaseAPIView
|
from plane.app.views.base import BaseViewSet, BaseAPIView
|
||||||
from plane.db.models import User, IssueActivity, WorkspaceMember, ProjectMember
|
from plane.db.models import User, IssueActivity, WorkspaceMember, ProjectMember, UserAsset
|
||||||
from plane.license.models import Instance, InstanceAdmin
|
from plane.license.models import Instance, InstanceAdmin
|
||||||
from plane.utils.paginator import BasePaginator
|
from plane.utils.paginator import BasePaginator
|
||||||
|
from plane.utils.file_stream import get_file_streams
|
||||||
|
|
||||||
from django.db.models import Q, F, Count, Case, When, IntegerField
|
from django.db.models import Q, F, Count, Case, When, IntegerField
|
||||||
|
|
||||||
@ -177,3 +186,27 @@ class UserActivityEndpoint(BaseAPIView, BasePaginator):
|
|||||||
issue_activities, many=True
|
issue_activities, many=True
|
||||||
).data,
|
).data,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class UserAssetsEndpoint(BaseAPIView):
|
||||||
|
parser_classes = (MultiPartParser, FormParser)
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
serializer = UserAssetSerializer(data=request.data)
|
||||||
|
if serializer.is_valid():
|
||||||
|
serializer.save(user=request.user)
|
||||||
|
return Response(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()
|
||||||
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||||
|
|
||||||
|
|
||||||
|
def get(self, request, key):
|
||||||
|
response = get_file_streams(key, key)
|
||||||
|
return response
|
||||||
|
175
apiserver/plane/db/migrations/0059_auto_20240131_1334.py
Normal file
175
apiserver/plane/db/migrations/0059_auto_20240131_1334.py
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
# Generated by Django 4.2.7 on 2024-01-31 13:34
|
||||||
|
import uuid
|
||||||
|
from django.db import migrations, models
|
||||||
|
from django.conf import settings
|
||||||
|
import django.db.models
|
||||||
|
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 = (
|
||||||
|
f"{settings.AWS_S3_URL_PROTOCOL}//{settings.AWS_S3_CUSTOM_DOMAIN}/"
|
||||||
|
)
|
||||||
|
prefix2 = prefix
|
||||||
|
else:
|
||||||
|
prefix = 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,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
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):
|
||||||
|
dependencies = [
|
||||||
|
("db", "0058_alter_moduleissue_issue_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
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",
|
||||||
|
field=models.FileField(
|
||||||
|
storage=plane.settings.storage.S3PrivateBucketStorage(),
|
||||||
|
upload_to=plane.db.models.asset.get_upload_path,
|
||||||
|
validators=[plane.db.models.asset.file_size],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="issueactivity",
|
||||||
|
name="attachments",
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="issuecomment",
|
||||||
|
name="attachments",
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="integration",
|
||||||
|
name="avatar_url",
|
||||||
|
field=models.CharField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="project",
|
||||||
|
name="cover_image",
|
||||||
|
field=models.CharField(blank=True, max_length=800, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="user",
|
||||||
|
name="cover_image",
|
||||||
|
field=models.CharField(blank=True, max_length=800, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="workspace",
|
||||||
|
name="logo",
|
||||||
|
field=models.CharField(blank=True, null=True, verbose_name="Logo"),
|
||||||
|
),
|
||||||
|
migrations.RunPython(update_urls),
|
||||||
|
]
|
@ -1,6 +1,6 @@
|
|||||||
from .base import BaseModel
|
from .base import BaseModel
|
||||||
|
|
||||||
from .user import User
|
from .user import User, UserAsset
|
||||||
|
|
||||||
from .workspace import (
|
from .workspace import (
|
||||||
Workspace,
|
Workspace,
|
||||||
|
@ -29,7 +29,7 @@ class Integration(AuditModel):
|
|||||||
redirect_url = models.TextField(blank=True)
|
redirect_url = models.TextField(blank=True)
|
||||||
metadata = models.JSONField(default=dict)
|
metadata = models.JSONField(default=dict)
|
||||||
verified = models.BooleanField(default=False)
|
verified = models.BooleanField(default=False)
|
||||||
avatar_url = models.URLField(blank=True, null=True)
|
avatar_url = models.CharField(blank=True, null=True)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
"""Return provider of the integration"""
|
"""Return provider of the integration"""
|
||||||
|
@ -389,9 +389,6 @@ class IssueActivity(ProjectBaseModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
comment = models.TextField(verbose_name="Comment", blank=True)
|
comment = models.TextField(verbose_name="Comment", blank=True)
|
||||||
attachments = ArrayField(
|
|
||||||
models.URLField(), size=10, blank=True, default=list
|
|
||||||
)
|
|
||||||
issue_comment = models.ForeignKey(
|
issue_comment = models.ForeignKey(
|
||||||
"db.IssueComment",
|
"db.IssueComment",
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
@ -423,9 +420,6 @@ class IssueComment(ProjectBaseModel):
|
|||||||
comment_stripped = models.TextField(verbose_name="Comment", blank=True)
|
comment_stripped = models.TextField(verbose_name="Comment", blank=True)
|
||||||
comment_json = models.JSONField(blank=True, default=dict)
|
comment_json = models.JSONField(blank=True, default=dict)
|
||||||
comment_html = models.TextField(blank=True, default="<p></p>")
|
comment_html = models.TextField(blank=True, default="<p></p>")
|
||||||
attachments = ArrayField(
|
|
||||||
models.URLField(), size=10, blank=True, default=list
|
|
||||||
)
|
|
||||||
issue = models.ForeignKey(
|
issue = models.ForeignKey(
|
||||||
Issue, on_delete=models.CASCADE, related_name="issue_comments"
|
Issue, on_delete=models.CASCADE, related_name="issue_comments"
|
||||||
)
|
)
|
||||||
|
@ -94,7 +94,7 @@ class Project(BaseModel):
|
|||||||
issue_views_view = models.BooleanField(default=True)
|
issue_views_view = models.BooleanField(default=True)
|
||||||
page_view = models.BooleanField(default=True)
|
page_view = models.BooleanField(default=True)
|
||||||
inbox_view = models.BooleanField(default=False)
|
inbox_view = models.BooleanField(default=False)
|
||||||
cover_image = models.URLField(blank=True, null=True, max_length=800)
|
cover_image = models.CharField(blank=True, null=True, max_length=800)
|
||||||
estimate = models.ForeignKey(
|
estimate = models.ForeignKey(
|
||||||
"db.Estimate",
|
"db.Estimate",
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
|
@ -15,12 +15,17 @@ from django.db.models.signals import post_save
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
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
|
||||||
|
|
||||||
# Third party imports
|
# Third party imports
|
||||||
from sentry_sdk import capture_exception
|
from sentry_sdk import capture_exception
|
||||||
from slack_sdk import WebClient
|
from slack_sdk import WebClient
|
||||||
from slack_sdk.errors import SlackApiError
|
from slack_sdk.errors import SlackApiError
|
||||||
|
|
||||||
|
# Module imports
|
||||||
|
from . import BaseModel
|
||||||
|
from plane.settings.storage import S3PrivateBucketStorage
|
||||||
|
|
||||||
|
|
||||||
def get_default_onboarding():
|
def get_default_onboarding():
|
||||||
return {
|
return {
|
||||||
@ -49,7 +54,7 @@ class User(AbstractBaseUser, PermissionsMixin):
|
|||||||
first_name = models.CharField(max_length=255, blank=True)
|
first_name = models.CharField(max_length=255, blank=True)
|
||||||
last_name = models.CharField(max_length=255, blank=True)
|
last_name = models.CharField(max_length=255, blank=True)
|
||||||
avatar = models.CharField(max_length=255, blank=True)
|
avatar = models.CharField(max_length=255, blank=True)
|
||||||
cover_image = models.URLField(blank=True, null=True, max_length=800)
|
cover_image = models.CharField(blank=True, null=True, max_length=800)
|
||||||
|
|
||||||
# tracking metrics
|
# tracking metrics
|
||||||
date_joined = models.DateTimeField(
|
date_joined = models.DateTimeField(
|
||||||
@ -144,6 +149,42 @@ class User(AbstractBaseUser, PermissionsMixin):
|
|||||||
super(User, self).save(*args, **kwargs)
|
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)
|
@receiver(post_save, sender=User)
|
||||||
def send_welcome_slack(sender, instance, created, **kwargs):
|
def send_welcome_slack(sender, instance, created, **kwargs):
|
||||||
try:
|
try:
|
||||||
|
@ -131,7 +131,7 @@ def slug_validator(value):
|
|||||||
|
|
||||||
class Workspace(BaseModel):
|
class Workspace(BaseModel):
|
||||||
name = models.CharField(max_length=80, verbose_name="Workspace Name")
|
name = models.CharField(max_length=80, verbose_name="Workspace Name")
|
||||||
logo = models.URLField(verbose_name="Logo", blank=True, null=True)
|
logo = models.CharField(verbose_name="Logo", blank=True, null=True)
|
||||||
owner = models.ForeignKey(
|
owner = models.ForeignKey(
|
||||||
settings.AUTH_USER_MODEL,
|
settings.AUTH_USER_MODEL,
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
|
@ -1,15 +1,9 @@
|
|||||||
# Third party imports
|
# Third party imports
|
||||||
from storages.backends.s3boto3 import S3Boto3Storage
|
from storages.backends.s3boto3 import S3Boto3Storage
|
||||||
|
|
||||||
# Module imports
|
|
||||||
from plane.utils.presigned_url_generator import generate_download_presigned_url
|
|
||||||
|
|
||||||
|
|
||||||
class S3PrivateBucketStorage(S3Boto3Storage):
|
class S3PrivateBucketStorage(S3Boto3Storage):
|
||||||
|
|
||||||
def url(self, name):
|
def url(self, name):
|
||||||
# Return an empty string or None, or implement custom logic here
|
# Return an empty string or None, or implement custom logic here
|
||||||
return name
|
return name
|
||||||
|
|
||||||
def download_url(self, name):
|
|
||||||
return generate_download_presigned_url(name)
|
|
||||||
|
59
apiserver/plane/utils/file_stream.py
Normal file
59
apiserver/plane/utils/file_stream.py
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
# Python imports
|
||||||
|
import uuid
|
||||||
|
import requests
|
||||||
|
|
||||||
|
# Django imports
|
||||||
|
from django.conf import settings
|
||||||
|
from django.http import StreamingHttpResponse
|
||||||
|
# Third party imports
|
||||||
|
import boto3
|
||||||
|
|
||||||
|
|
||||||
|
def get_file_streams(key, filename=uuid.uuid4().hex):
|
||||||
|
|
||||||
|
if settings.USE_MINIO:
|
||||||
|
s3 = boto3.client(
|
||||||
|
's3',
|
||||||
|
endpoint_url=settings.AWS_S3_ENDPOINT_URL,
|
||||||
|
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
|
||||||
|
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
|
||||||
|
config=boto3.session.Config(signature_version='s3v4'),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
s3 = boto3.client(
|
||||||
|
's3',
|
||||||
|
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
|
||||||
|
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
|
||||||
|
config=boto3.session.Config(signature_version='s3v4'),
|
||||||
|
)
|
||||||
|
|
||||||
|
presigned_url = s3.generate_presigned_url(
|
||||||
|
'get_object',
|
||||||
|
Params={
|
||||||
|
'Bucket': settings.AWS_STORAGE_BUCKET_NAME,
|
||||||
|
'Key': key,
|
||||||
|
},
|
||||||
|
ExpiresIn=3600,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Fetch the object metadata to get the content type
|
||||||
|
metadata = s3.head_object(
|
||||||
|
Bucket=settings.AWS_STORAGE_BUCKET_NAME,
|
||||||
|
Key=key,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Stream the file from the custom endpoint URL
|
||||||
|
def stream_file_from_url(url):
|
||||||
|
with requests.get(url, stream=True) as r:
|
||||||
|
r.raise_for_status()
|
||||||
|
for chunk in r.iter_content(chunk_size=8192):
|
||||||
|
if chunk: # filter out keep-alive new chunks
|
||||||
|
yield chunk
|
||||||
|
|
||||||
|
|
||||||
|
content_type = metadata['ContentType']
|
||||||
|
response = StreamingHttpResponse(stream_file_from_url(presigned_url), content_type=content_type)
|
||||||
|
response['Content-Disposition'] = f'inline; filename={filename}' # Adjust filename as needed
|
||||||
|
|
||||||
|
return response
|
Loading…
Reference in New Issue
Block a user