Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Profile Picture Support for Users and Groups #12

Open
wants to merge 4 commits into
base: main
Choose a base branch
from

Conversation

itsumarfarooq
Copy link

Overview

This PR introduces profile picture functionality for both users and groups, allowing them to upload, update, and delete profile pictures. The implementation includes a new media app for handling file uploads and a reusable ProfilePictureMixin for shared functionality.

Key Changes

  • Created new media Django app for handling file uploads
  • Added ProfilePictureMixin for shared profile picture functionality
  • Implemented profile picture support for User and Group models
  • Added API endpoints for uploading/updating profile pictures
  • Set up file size limits and allowed image extensions
  • Added comprehensive test script to verify functionality

Implementation Details

Media App

  • Created new Media model for handling uploaded files
  • Implemented generic foreign key relationship for flexibility
  • Added file validation for size and extensions
  • Set up proper file organization with dynamic upload paths

Profile Picture Support

  • Added ProfilePictureMixin for shared functionality:
    • update_profile_picture() method handles updates and cleanup
    • Automatic deletion of old profile pictures
  • Integrated with User and Group models
  • Added new API endpoints:
    • /api/user/profile-picture
    • /api/groups/<uuid:group_uid>/profile-picture

Configuration

  • Set maximum upload size to 5MB
  • Allowed image extensions: .jpg, .jpeg, .png, .gif
  • Files are stored in uploads/<model>/<uuid>.<extension>

Testing

Included a comprehensive test script that verifies:

  • Initial profile picture upload
  • Profile picture updates (with old file cleanup)
  • Profile picture deletion
  • Group profile picture functionality
  • Error handling and edge cases

Test results show successful implementation of all core functionality:

  • ✓ File upload and storage
  • ✓ Automatic cleanup of old files
  • ✓ Reference management
  • ✓ Group-specific functionality

API Changes

New endpoints added:

POST /api/media/<model_type>/<model_id>/upload
DELETE /api/media/<uuid:media_uid>
PUT /api/user/profile-picture
PUT /api/groups/<uuid:group_uid>/profile-picture

Migration Guide

  1. Run migrations:
    python manage.py migrate
  2. Update environment settings:
    MAX_UPLOAD_SIZE = 5 * 1024 * 1024  # 5MB
    ALLOWED_IMAGE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif']

Testing Instructions

  1. Tested the functionality using following script:
#profile_picture_test.py

import os
import sys
from pathlib import Path

# Add the parent directory to Python path
current_dir = os.path.dirname(os.path.abspath(__file__))
parent_dir = os.path.dirname(current_dir)
sys.path.append(parent_dir)

# Configure Django settings BEFORE importing any Django modules
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'splinter.settings')

# Now we can import and setup Django
import django
django.setup()

# After Django is configured, we can import Django models
from django.contrib.contenttypes.models import ContentType
from django.core.files.uploadedfile import SimpleUploadedFile
from splinter.apps.user.models import User
from splinter.apps.group.models import Group
from splinter.apps.media.models import Media

def create_test_user():
    """Create a test user if it doesn't exist"""
    user, created = User.objects.get_or_create(
        username='test_user',
        defaults={
            'email': '[email protected]',
            'first_name': 'Test',
            'last_name': 'User'
        }
    )
    if created:
        user.set_password('testpass123')
        user.save()
    return user

def create_test_group(created_by):
    """Create a test group if it doesn't exist"""
    group, created = Group.objects.get_or_create(
        name='Test Group',
        defaults={'created_by': created_by}
    )
    return group

def upload_profile_picture(instance, image_path):
    """
    Upload a profile picture for a given model instance (User or Group)
    """
    try:
        with open(image_path, 'rb') as img_file:
            uploaded_file = SimpleUploadedFile(
                name=os.path.basename(image_path),
                content=img_file.read(),
                content_type='image/jpeg'
            )
            
            content_type = ContentType.objects.get_for_model(instance)
            
            media = Media.objects.create(
                file=uploaded_file,
                content_type=content_type,
                object_id=instance.id,
                uploaded_by=instance if isinstance(instance, User) else instance.created_by
            )
            
            instance.update_profile_picture(media)
            return media
            
    except Exception as e:
        print(f"Error uploading profile picture: {e}")
        raise

def delete_profile_picture(instance):
    """
    Delete profile picture for a given model instance (User or Group)
    """
    if instance.profile_picture:
        media = instance.profile_picture
        # First clear the reference
        instance.profile_picture = None
        instance.save()
        # Then delete the media
        media.delete()
        return True
    return False

def test_profile_picture_lifecycle(image_path1, image_path2):
    """
    Test complete lifecycle of profile pictures including upload, update, and delete
    """
    if not os.path.exists(image_path1) or not os.path.exists(image_path2):
        print(f"Error: One or both image files not found")
        return False
        
    try:
        # Create test user
        print("Creating test user...")
        user = create_test_user()
        print(f"Created/Retrieved user: {user.username}")
        
        # Test 1: Initial Upload
        print("\n=== Testing Initial Upload ===")
        user_media = upload_profile_picture(user, image_path1)
        print(f"Initial profile picture uploaded successfully")
        print(f"Media URL: {user_media.url}")
        initial_media_id = user_media.id
        
        # Test 2: Update Profile Picture
        print("\n=== Testing Profile Picture Update ===")
        updated_media = upload_profile_picture(user, image_path2)
        print(f"Updated profile picture uploaded successfully")
        print(f"New Media URL: {updated_media.url}")
        
        # Verify old media was deleted
        try:
            Media.objects.get(id=initial_media_id)
            print("❌ Old media still exists - cleanup failed")
            success = False
        except Media.DoesNotExist:
            print("✓ Old media properly cleaned up")
            success = True
            
        # Test 3: Delete Profile Picture
        print("\n=== Testing Profile Picture Deletion ===")
        if updated_media:
            media_id = updated_media.id
            delete_profile_picture(user)
            
            # Verify media was deleted
            try:
                Media.objects.get(id=media_id)
                print("❌ Media deletion failed")
                success = False
            except Media.DoesNotExist:
                print("✓ Media successfully deleted")
                success = True
                
            # Verify user reference is cleared
            user.refresh_from_db()
            if user.profile_picture is None:
                print("✓ User profile picture reference cleared")
            else:
                print("❌ User profile picture reference still exists")
                success = False
            
        # Test Group Profile Picture
        print("\n=== Testing Group Profile Picture ===")
        group = create_test_group(user)
        print(f"Created/Retrieved group: {group.name}")
        
        # Upload and verify
        group_media = upload_profile_picture(group, image_path1)
        print(f"Group profile picture uploaded successfully")
        print(f"Media URL: {group_media.url}")
        
        # Update group picture
        print("\n=== Testing Group Profile Picture Update ===")
        initial_group_media_id = group_media.id
        updated_group_media = upload_profile_picture(group, image_path2)
        print(f"Updated group profile picture")
        print(f"New Media URL: {updated_group_media.url}")
        
        # Verify old group media was deleted
        try:
            Media.objects.get(id=initial_group_media_id)
            print("❌ Old group media still exists - cleanup failed")
            success = False
        except Media.DoesNotExist:
            print("✓ Old group media properly cleaned up")
            
        # Test Group Profile Picture Deletion
        print("\n=== Testing Group Profile Picture Deletion ===")
        if delete_profile_picture(group):
            group.refresh_from_db()
            if group.profile_picture is None:
                print("✓ Group profile picture reference cleared")
            else:
                print("❌ Group profile picture reference still exists")
                success = False
        
        return success
            
    except Exception as e:
        print(f"\nTest failed with error: {e}")
        import traceback
        traceback.print_exc()
        return False

def main():
    if len(sys.argv) != 3:
        print("Usage: python profile_picture_test.py <image_path1> <image_path2>")
        print("Example: python profile_picture_test.py ./test1.jpg ./test2.jpg")
        sys.exit(1)
    
    image_path1 = sys.argv[1]
    image_path2 = sys.argv[2]
    
    print("=== Starting Profile Picture Lifecycle Test ===")
    success = test_profile_picture_lifecycle(image_path1, image_path2)
    print("\n=== Test Complete ===")
    print("Result:", "Success" if success else "Failure")
    sys.exit(0 if success else 1)

if __name__ == '__main__':
    main()
  1. Run the test script:
    python profile_picture_test.py ./test1.jpg ./test2.jpg

Notes

  • Profile pictures are optional for both users and groups
  • Old profile pictures are automatically deleted when updated
  • File validations are enforced on both backend and frontend

Related Issues

Closes #11

Add new media app and configure basic upload settings including:
- Register new media app in INSTALLED_APPS
- Configure max upload size (5MB) and allowed image extensions
- Add media URLs to main URL configuration
- Extend base settings with media-specific configurations

The changes lay the groundwork for handling media file uploads in the application.
- Add profile_picture OneToOneField to User and Group models
- Create ProfilePictureMixin for shared profile picture functionality
- Add new endpoints for uploading profile pictures (/user/profile-picture and /groups/<uid>/profile-picture)
- Update serializers to include profile picture data in responses
- Add migrations for new profile picture fields

The profile pictures are stored as Media objects and include cleanup of old pictures when updated.
@uadnan uadnan force-pushed the main branch 5 times, most recently from 7b6232e to b489fa2 Compare January 7, 2025 19:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

(feat) Add support for images on backend.
1 participant