# /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