uchill/chatnext/backend/apps/accounts/api/views/profile_avatar_views.py

273 lines
11 KiB
Python

# /home/ram/aparsoft/backend/apps/accounts/api/views/profile_avatar_views.py
"""
Profile Avatar Views for AparSoft
This module handles avatar upload, update, and removal functionality
with image optimization and security checks.
"""
from rest_framework import status
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from rest_framework.views import APIView
from django.contrib.auth import get_user_model
from django.core.files.storage import default_storage
from django.core.files.base import ContentFile
from PIL import Image
import os
import uuid
import logging
from io import BytesIO
from ...utils.profile_picture_utils import (
has_profile_picture_field,
get_profile_picture_url,
set_profile_picture,
delete_profile_picture,
get_user_profile_data,
)
logger = logging.getLogger(__name__)
User = get_user_model()
class ProfileAvatarView(APIView):
"""
Handle avatar upload, update, and removal for user profiles.
"""
permission_classes = [IsAuthenticated]
def get(self, request):
"""Get current user's profile image URL."""
try:
user = request.user
# Use utility function to safely get profile picture URL
profile_picture_url = get_profile_picture_url(user, request)
if profile_picture_url:
return Response({
'message': 'Profile image found',
'status': 'success',
'data': {
'profile_picture_url': profile_picture_url
}
}, status=status.HTTP_200_OK)
else:
# Check if the field exists to provide appropriate message
if not has_profile_picture_field(user):
return Response({
'message': 'Profile picture feature not available. Please run database migrations.',
'status': 'error',
'data': {
'profile_picture_url': None
},
'error_code': 'FIELD_NOT_FOUND'
}, status=status.HTTP_503_SERVICE_UNAVAILABLE)
else:
return Response({
'message': 'No profile image found',
'status': 'info',
'data': {
'profile_picture_url': None
}
}, status=status.HTTP_200_OK)
except Exception as e:
logger.error(f"Profile image retrieval error for user {request.user.id}: {str(e)}", exc_info=True)
return Response({
'message': 'Failed to retrieve profile image',
'status': 'error',
'data': {
'profile_picture_url': None
},
'error_code': 'RETRIEVAL_ERROR'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
def post(self, request):
"""Upload/update user avatar."""
try:
user = request.user
# Check if profile_picture field exists
if not has_profile_picture_field(user):
return Response({
'message': 'Profile picture feature not available. Please run database migrations.',
'status': 'error',
'error_code': 'FIELD_NOT_FOUND'
}, status=status.HTTP_503_SERVICE_UNAVAILABLE)
# Check if file was uploaded
if 'profile_picture' not in request.FILES:
return Response({
'message': 'No image file provided',
'status': 'error'
}, status=status.HTTP_400_BAD_REQUEST)
image_file = request.FILES['profile_picture']
# Validate file type
allowed_types = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp']
if image_file.content_type not in allowed_types:
return Response({
'message': 'Invalid file type. Only JPEG, PNG, and WebP are allowed.',
'status': 'error'
}, status=status.HTTP_400_BAD_REQUEST)
# Validate file size (5MB max)
max_size = 5 * 1024 * 1024 # 5MB
if image_file.size > max_size:
return Response({
'message': 'File size too large. Maximum 5MB allowed.',
'status': 'error'
}, status=status.HTTP_400_BAD_REQUEST)
# Process and optimize image
try:
optimized_image = self.optimize_image(image_file)
except Exception as e:
logger.error(f"Image optimization failed for user {user.id}: {str(e)}")
return Response({
'message': 'Failed to process image. Please try a different image.',
'status': 'error'
}, status=status.HTTP_400_BAD_REQUEST)
# Delete old avatar if exists (handled in set_profile_picture utility)
# Generate unique filename
file_extension = 'jpg' # Always save as JPEG after optimization
filename = f"avatars/user_{user.id}_{uuid.uuid4().hex[:8]}.{file_extension}"
# Save optimized image
saved_path = default_storage.save(filename, ContentFile(optimized_image.getvalue()))
# Update user model using utility function
success = set_profile_picture(user, saved_path)
if not success:
# Clean up the file if setting failed
try:
default_storage.delete(saved_path)
except Exception:
pass
return Response({
'message': 'Failed to save avatar',
'status': 'error'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
# Generate full URL
avatar_url = request.build_absolute_uri(default_storage.url(saved_path))
logger.info(f"Avatar updated successfully for user {user.id}")
return Response({
'message': 'Avatar updated successfully',
'status': 'success',
'data': {
'profile_picture_url': avatar_url
}
}, status=status.HTTP_200_OK)
except Exception as e:
logger.error(f"Avatar upload error for user {request.user.id}: {str(e)}", exc_info=True)
return Response({
'message': 'Failed to upload avatar',
'status': 'error',
'error_code': 'UPLOAD_ERROR'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
def delete(self, request):
"""Remove user avatar."""
try:
user = request.user
# Check if profile_picture field exists
if not has_profile_picture_field(user):
return Response({
'message': 'Profile picture feature not available. Please run database migrations.',
'status': 'error',
'error_code': 'FIELD_NOT_FOUND'
}, status=status.HTTP_503_SERVICE_UNAVAILABLE)
# Use utility function to safely delete profile picture
success = delete_profile_picture(user)
if success:
return Response({
'message': 'Avatar removed successfully',
'status': 'success'
}, status=status.HTTP_200_OK)
else:
return Response({
'message': 'Failed to remove avatar',
'status': 'error',
'error_code': 'DELETION_ERROR'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
except Exception as e:
logger.error(f"Avatar removal error for user {request.user.id}: {str(e)}", exc_info=True)
return Response({
'message': 'Failed to remove avatar',
'status': 'error',
'error_code': 'DELETION_ERROR'
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
def optimize_image(self, image_file, size=(200, 200), quality=85):
"""
Optimize uploaded image for avatar use.
Args:
image_file: Uploaded image file
size: Target size tuple (width, height)
quality: JPEG quality (1-100)
Returns:
BytesIO: Optimized image data
"""
try:
# Open image with PIL
with Image.open(image_file) as img:
# Convert to RGB if necessary (handles PNG with transparency, etc.)
if img.mode in ('RGBA', 'LA', 'P'):
# Create white background
background = Image.new('RGB', img.size, (255, 255, 255))
if img.mode == 'P':
img = img.convert('RGBA')
background.paste(img, mask=img.split()[-1] if img.mode == 'RGBA' else None)
img = background
elif img.mode != 'RGB':
img = img.convert('RGB')
# Calculate dimensions for center crop to square
width, height = img.size
if width > height:
# Landscape: crop sides
left = (width - height) // 2
top = 0
right = left + height
bottom = height
else:
# Portrait: crop top/bottom
left = 0
top = (height - width) // 2
right = width
bottom = top + width
# Crop to square
img = img.crop((left, top, right, bottom))
# Resize to target size with high-quality resampling
img = img.resize(size, Image.Resampling.LANCZOS)
# Save optimized image to BytesIO
output = BytesIO()
img.save(output, format='JPEG', quality=quality, optimize=True)
output.seek(0)
return output
except Exception as e:
logger.error(f"Image optimization error: {str(e)}")
raise