feat(workspaces): add thumbnail upload and lifecycle support
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
import json
|
||||||
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
@@ -16,7 +17,8 @@ class WorkspaceMemberInputSerializer(serializers.Serializer):
|
|||||||
|
|
||||||
|
|
||||||
class WorkspaceSerializer(BaseModelSerializer):
|
class WorkspaceSerializer(BaseModelSerializer):
|
||||||
members = WorkspaceMemberInputSerializer(many=True, write_only=True, required=False)
|
members = serializers.JSONField(write_only=True, required=False)
|
||||||
|
clear_thumbnail = serializers.BooleanField(write_only=True, required=False, default=False)
|
||||||
my_role = serializers.SerializerMethodField()
|
my_role = serializers.SerializerMethodField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -24,6 +26,8 @@ class WorkspaceSerializer(BaseModelSerializer):
|
|||||||
fields = BaseModelSerializer.Meta.fields + (
|
fields = BaseModelSerializer.Meta.fields + (
|
||||||
"name",
|
"name",
|
||||||
"description",
|
"description",
|
||||||
|
"thumbnail",
|
||||||
|
"clear_thumbnail",
|
||||||
"owner",
|
"owner",
|
||||||
"my_role",
|
"my_role",
|
||||||
"members",
|
"members",
|
||||||
@@ -39,8 +43,41 @@ class WorkspaceSerializer(BaseModelSerializer):
|
|||||||
).first()
|
).first()
|
||||||
return getattr(membership, "role", None)
|
return getattr(membership, "role", None)
|
||||||
|
|
||||||
|
def validate_thumbnail(self, value):
|
||||||
|
if value is None:
|
||||||
|
return value
|
||||||
|
max_bytes = 2 * 1024 * 1024
|
||||||
|
if getattr(value, "size", 0) > max_bytes:
|
||||||
|
raise serializers.ValidationError("Image size must be 2MB or less.")
|
||||||
|
content_type = (getattr(value, "content_type", "") or "").lower()
|
||||||
|
allowed_types = {"image/jpeg", "image/png", "image/webp"}
|
||||||
|
if content_type and content_type not in allowed_types:
|
||||||
|
raise serializers.ValidationError("Unsupported image type. Use JPG, PNG, or WebP.")
|
||||||
|
return value
|
||||||
|
|
||||||
|
def to_representation(self, instance):
|
||||||
|
data = super().to_representation(instance)
|
||||||
|
request = self.context.get("request")
|
||||||
|
if instance.thumbnail:
|
||||||
|
thumbnail_url = instance.thumbnail.url
|
||||||
|
data["thumbnail"] = request.build_absolute_uri(thumbnail_url) if request else thumbnail_url
|
||||||
|
else:
|
||||||
|
data["thumbnail"] = None
|
||||||
|
data.pop("clear_thumbnail", None)
|
||||||
|
return data
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
members_data = validated_data.pop('members', [])
|
members_data = validated_data.pop("members", [])
|
||||||
|
if isinstance(members_data, str):
|
||||||
|
try:
|
||||||
|
members_data = json.loads(members_data)
|
||||||
|
except json.JSONDecodeError as exc:
|
||||||
|
raise serializers.ValidationError({"members": "Invalid members format."}) from exc
|
||||||
|
if members_data:
|
||||||
|
members_serializer = WorkspaceMemberInputSerializer(data=members_data, many=True)
|
||||||
|
members_serializer.is_valid(raise_exception=True)
|
||||||
|
members_data = members_serializer.validated_data
|
||||||
|
validated_data.pop("clear_thumbnail", None)
|
||||||
|
|
||||||
workspace = super().create(validated_data)
|
workspace = super().create(validated_data)
|
||||||
|
|
||||||
@@ -75,6 +112,23 @@ class WorkspaceSerializer(BaseModelSerializer):
|
|||||||
|
|
||||||
return workspace
|
return workspace
|
||||||
|
|
||||||
|
def update(self, instance, validated_data):
|
||||||
|
clear_thumbnail = validated_data.pop("clear_thumbnail", False)
|
||||||
|
old_thumbnail_name = instance.thumbnail.name if instance.thumbnail else None
|
||||||
|
|
||||||
|
if clear_thumbnail and instance.thumbnail:
|
||||||
|
instance.thumbnail.delete(save=False)
|
||||||
|
instance.thumbnail = None
|
||||||
|
|
||||||
|
updated_workspace = super().update(instance, validated_data)
|
||||||
|
|
||||||
|
if old_thumbnail_name and updated_workspace.thumbnail and updated_workspace.thumbnail.name != old_thumbnail_name:
|
||||||
|
storage = updated_workspace.thumbnail.storage
|
||||||
|
if storage.exists(old_thumbnail_name):
|
||||||
|
storage.delete(old_thumbnail_name)
|
||||||
|
|
||||||
|
return updated_workspace
|
||||||
|
|
||||||
|
|
||||||
class WorkspaceMembershipSerializer(BaseModelSerializer):
|
class WorkspaceMembershipSerializer(BaseModelSerializer):
|
||||||
user = serializers.SerializerMethodField()
|
user = serializers.SerializerMethodField()
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from rest_framework.filters import OrderingFilter, SearchFilter
|
|||||||
from django_filters.rest_framework import DjangoFilterBackend
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
from rest_framework.permissions import IsAuthenticated
|
from rest_framework.permissions import IsAuthenticated
|
||||||
|
from rest_framework.parsers import FormParser, MultiPartParser, JSONParser
|
||||||
|
|
||||||
from apps.notifications.services import (
|
from apps.notifications.services import (
|
||||||
notify_workspace_membership_added,
|
notify_workspace_membership_added,
|
||||||
@@ -45,6 +46,7 @@ from core.paginations.limit_offset import CustomLimitOffsetPagination
|
|||||||
|
|
||||||
class WorkspaceViewSet(ModelViewSet):
|
class WorkspaceViewSet(ModelViewSet):
|
||||||
serializer_class = WorkspaceSerializer
|
serializer_class = WorkspaceSerializer
|
||||||
|
parser_classes = [MultiPartParser, FormParser, JSONParser]
|
||||||
pagination_class = CustomLimitOffsetPagination
|
pagination_class = CustomLimitOffsetPagination
|
||||||
filter_backends = (DjangoFilterBackend, OrderingFilter, SearchFilter)
|
filter_backends = (DjangoFilterBackend, OrderingFilter, SearchFilter)
|
||||||
filterset_class = WorkspaceFilter
|
filterset_class = WorkspaceFilter
|
||||||
|
|||||||
17
apps/workspaces/migrations/0006_workspace_thumbnail.py
Normal file
17
apps/workspaces/migrations/0006_workspace_thumbnail.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("workspaces", "0005_remove_priceunit_priceunit_id_idx_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="workspace",
|
||||||
|
name="thumbnail",
|
||||||
|
field=models.ImageField(blank=True, null=True, upload_to="profile/workspaces/"),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
@@ -9,6 +9,7 @@ User = get_user_model()
|
|||||||
class Workspace(BaseModel):
|
class Workspace(BaseModel):
|
||||||
name = models.CharField(max_length=255)
|
name = models.CharField(max_length=255)
|
||||||
description = models.TextField(blank=True)
|
description = models.TextField(blank=True)
|
||||||
|
thumbnail = models.ImageField(upload_to="profile/workspaces/", blank=True, null=True)
|
||||||
owner = models.ForeignKey(
|
owner = models.ForeignKey(
|
||||||
User,
|
User,
|
||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
|
|||||||
Reference in New Issue
Block a user