feat(media): add client and project thumbnails
This commit is contained in:
@@ -6,6 +6,19 @@ from apps.projects.models import Project
|
||||
from apps.workspaces.models import PriceUnit
|
||||
|
||||
|
||||
def validate_thumbnail(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
|
||||
|
||||
|
||||
class ProjectSerializer(BaseModelSerializer):
|
||||
class Meta:
|
||||
model = Project
|
||||
@@ -14,6 +27,7 @@ class ProjectSerializer(BaseModelSerializer):
|
||||
"name",
|
||||
"client",
|
||||
"description",
|
||||
"thumbnail",
|
||||
"is_archived",
|
||||
"color",
|
||||
)
|
||||
@@ -21,12 +35,25 @@ class ProjectSerializer(BaseModelSerializer):
|
||||
|
||||
def to_representation(self, instance):
|
||||
representation = super().to_representation(instance)
|
||||
request = self.context.get("request")
|
||||
if instance.thumbnail:
|
||||
thumbnail_url = instance.thumbnail.url
|
||||
representation["thumbnail"] = request.build_absolute_uri(thumbnail_url) if request else thumbnail_url
|
||||
else:
|
||||
representation["thumbnail"] = None
|
||||
if instance.client:
|
||||
representation['client'] = {
|
||||
'id': instance.client.id,
|
||||
'name': instance.client.name
|
||||
}
|
||||
return representation
|
||||
'id': instance.client.id,
|
||||
'name': instance.client.name,
|
||||
'thumbnail': (
|
||||
request.build_absolute_uri(instance.client.thumbnail.url)
|
||||
if request and instance.client.thumbnail
|
||||
else instance.client.thumbnail.url
|
||||
if instance.client.thumbnail
|
||||
else None
|
||||
),
|
||||
}
|
||||
return representation
|
||||
|
||||
|
||||
class ProjectCreateSerializer(serializers.Serializer):
|
||||
@@ -34,16 +61,25 @@ class ProjectCreateSerializer(serializers.Serializer):
|
||||
name = serializers.CharField(max_length=255)
|
||||
client = serializers.UUIDField(required=False, allow_null=True)
|
||||
description = serializers.CharField(required=False, allow_blank=True, default="")
|
||||
thumbnail = serializers.ImageField(required=False, allow_null=True)
|
||||
color = serializers.CharField(max_length=7, required=False, allow_blank=True, default="")
|
||||
|
||||
def validate_thumbnail(self, value):
|
||||
return validate_thumbnail(value)
|
||||
|
||||
|
||||
class ProjectUpdateSerializer(serializers.Serializer):
|
||||
name = serializers.CharField(max_length=255, required=False)
|
||||
client = serializers.UUIDField(required=False, allow_null=True)
|
||||
description = serializers.CharField(required=False, allow_blank=True)
|
||||
thumbnail = serializers.ImageField(required=False, allow_null=True)
|
||||
clear_thumbnail = serializers.BooleanField(required=False, default=False)
|
||||
color = serializers.CharField(max_length=7, required=False, allow_blank=True)
|
||||
is_archived = serializers.BooleanField(required=False)
|
||||
|
||||
def validate_thumbnail(self, value):
|
||||
return validate_thumbnail(value)
|
||||
|
||||
|
||||
class ProjectAccessQuerySerializer(serializers.Serializer):
|
||||
workspace = serializers.UUIDField()
|
||||
|
||||
@@ -89,6 +89,9 @@ class ProjectViewSet(ModelViewSet):
|
||||
if client_ids:
|
||||
queryset = queryset.filter(client_id__in=client_ids)
|
||||
|
||||
if "is_archived" not in self.request.query_params:
|
||||
queryset = queryset.filter(is_archived=False)
|
||||
|
||||
return queryset
|
||||
|
||||
def get_serializer_class(self):
|
||||
@@ -120,13 +123,14 @@ class ProjectViewSet(ModelViewSet):
|
||||
project = create_project(
|
||||
user=request.user,
|
||||
workspace=workspace,
|
||||
name=serializer.validated_data["name"],
|
||||
client=client,
|
||||
name=serializer.validated_data["name"],
|
||||
client=client,
|
||||
description=serializer.validated_data.get("description", ""),
|
||||
color=serializer.validated_data.get("color", "")
|
||||
color=serializer.validated_data.get("color", ""),
|
||||
thumbnail=serializer.validated_data.get("thumbnail"),
|
||||
)
|
||||
|
||||
output_serializer = ProjectSerializer(project)
|
||||
output_serializer = ProjectSerializer(project, context={"request": request})
|
||||
return Response(output_serializer.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
def update(self, request, *args, **kwargs):
|
||||
@@ -144,7 +148,7 @@ class ProjectViewSet(ModelViewSet):
|
||||
**serializer.validated_data
|
||||
)
|
||||
|
||||
output_serializer = ProjectSerializer(updated_project)
|
||||
output_serializer = ProjectSerializer(updated_project, context={"request": request})
|
||||
return Response(output_serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
@@ -169,7 +173,7 @@ class ProjectViewSet(ModelViewSet):
|
||||
project = self.get_object()
|
||||
updated_project = toggle_project_archive(project)
|
||||
|
||||
output_serializer = ProjectSerializer(updated_project)
|
||||
output_serializer = ProjectSerializer(updated_project, context={"request": request})
|
||||
return Response(output_serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
@action(detail=False, methods=["get"], url_path="access")
|
||||
|
||||
18
apps/projects/migrations/0005_project_thumbnail.py
Normal file
18
apps/projects/migrations/0005_project_thumbnail.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.2.12 on 2026-05-26 08:20
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('projects', '0004_projectaccess'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='project',
|
||||
name='thumbnail',
|
||||
field=models.ImageField(blank=True, null=True, upload_to='profile/projects/'),
|
||||
),
|
||||
]
|
||||
@@ -28,6 +28,8 @@ class Project(BaseModel):
|
||||
|
||||
description = models.TextField(blank=True)
|
||||
|
||||
thumbnail = models.ImageField(upload_to="profile/projects/", blank=True, null=True)
|
||||
|
||||
is_archived = models.BooleanField(default=False)
|
||||
|
||||
color = models.CharField(max_length=7, blank=True)
|
||||
|
||||
@@ -7,8 +7,8 @@ from apps.projects.models import Project
|
||||
from apps.workspaces.models import WorkspaceMembership
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def create_project(user, workspace, name, client=None, description="", color=""):
|
||||
@transaction.atomic
|
||||
def create_project(user, workspace, name, client=None, description="", color="", thumbnail=None):
|
||||
"""
|
||||
Creates a new workspace-shared project.
|
||||
"""
|
||||
@@ -30,6 +30,7 @@ def create_project(user, workspace, name, client=None, description="", color="")
|
||||
name=name,
|
||||
client=client,
|
||||
description=description,
|
||||
thumbnail=thumbnail,
|
||||
color=color,
|
||||
created_by=user,
|
||||
updated_by=user,
|
||||
@@ -38,7 +39,7 @@ def create_project(user, workspace, name, client=None, description="", color="")
|
||||
return project
|
||||
|
||||
|
||||
def update_project(project, **kwargs):
|
||||
def update_project(project, **kwargs):
|
||||
"""
|
||||
Updates specific fields of an existing project.
|
||||
"""
|
||||
@@ -49,20 +50,32 @@ def update_project(project, **kwargs):
|
||||
if Project.objects.filter(workspace=project.workspace, name=kwargs["name"], is_deleted=False).exists():
|
||||
raise ValidationError({"name": "A project with this name already exists in the workspace."})
|
||||
|
||||
client_id = kwargs.pop("client")
|
||||
client = get_object_or_404(Client, id=client_id, is_deleted=False) if client_id else None
|
||||
kwargs["client"] = client
|
||||
if "client" in kwargs:
|
||||
client_id = kwargs.pop("client")
|
||||
client = get_object_or_404(Client, id=client_id, is_deleted=False) if client_id else None
|
||||
kwargs["client"] = client
|
||||
|
||||
clear_thumbnail = kwargs.pop("clear_thumbnail", False)
|
||||
old_thumbnail_name = project.thumbnail.name if project.thumbnail else None
|
||||
if clear_thumbnail and project.thumbnail:
|
||||
project.thumbnail.delete(save=False)
|
||||
project.thumbnail = None
|
||||
update_fields.append("thumbnail")
|
||||
|
||||
for field, value in kwargs.items():
|
||||
if hasattr(project, field) and getattr(project, field) != value:
|
||||
setattr(project, field, value)
|
||||
update_fields.append(field)
|
||||
for field, value in kwargs.items():
|
||||
if hasattr(project, field) and getattr(project, field) != value:
|
||||
setattr(project, field, value)
|
||||
update_fields.append(field)
|
||||
|
||||
if update_fields:
|
||||
update_fields.append("updated_at")
|
||||
project.save(update_fields=update_fields)
|
||||
|
||||
return project
|
||||
update_fields.append("updated_at")
|
||||
project.save(update_fields=update_fields)
|
||||
if old_thumbnail_name and project.thumbnail and project.thumbnail.name != old_thumbnail_name:
|
||||
storage = project.thumbnail.storage
|
||||
if storage.exists(old_thumbnail_name):
|
||||
storage.delete(old_thumbnail_name)
|
||||
|
||||
return project
|
||||
|
||||
|
||||
def toggle_project_archive(project) -> Project:
|
||||
|
||||
Reference in New Issue
Block a user