feat: Implement avatar upload system with Cloudflare integration

- Added migration to transition avatar data from CloudflareImageField to ForeignKey structure in UserProfile.
- Fixed UserProfileEvent avatar field to align with new avatar structure.
- Created serializers for social authentication, including connected and available providers.
- Developed request logging middleware for comprehensive request/response logging.
- Updated moderation and parks migrations to remove outdated triggers and adjust foreign key relationships.
- Enhanced rides migrations to ensure proper handling of image uploads and triggers.
- Introduced a test script for the 3-step avatar upload process, ensuring functionality with Cloudflare.
- Documented the fix for avatar upload issues, detailing root cause, implementation, and verification steps.
- Implemented automatic deletion of Cloudflare images upon avatar, park, and ride photo changes or removals.
This commit is contained in:
pacnpal
2025-08-30 21:20:25 -04:00
parent fb6726f89a
commit 9bed782784
75 changed files with 4571 additions and 1962 deletions

View File

@@ -10,7 +10,6 @@ from datetime import timedelta
from django.utils import timezone
from apps.core.history import TrackedModel
import pghistory
from cloudflare_images.field import CloudflareImagesField
def generate_random_id(model_class, id_field):
@@ -160,7 +159,12 @@ class UserProfile(models.Model):
blank=True,
help_text="Legacy display name field - use User.display_name instead",
)
avatar = CloudflareImagesField(blank=True, null=True)
avatar = models.ForeignKey(
'django_cloudflareimages_toolkit.CloudflareImage',
on_delete=models.SET_NULL,
null=True,
blank=True
)
pronouns = models.CharField(max_length=50, blank=True)
bio = models.TextField(max_length=500, blank=True)
@@ -181,12 +185,26 @@ class UserProfile(models.Model):
"""
Return the avatar URL or generate a default letter-based avatar URL
"""
if self.avatar:
# Return Cloudflare Images URL with avatar variant
base_url = self.avatar.url
if '/public' in base_url:
return base_url.replace('/public', '/avatar')
return base_url
if self.avatar and self.avatar.is_uploaded:
# Try to get avatar variant first, fallback to public
avatar_url = self.avatar.get_url('avatar')
if avatar_url:
return avatar_url
# Fallback to public variant
public_url = self.avatar.get_url('public')
if public_url:
return public_url
# Last fallback - try any available variant
if self.avatar.variants:
if isinstance(self.avatar.variants, list) and self.avatar.variants:
return self.avatar.variants[0]
elif isinstance(self.avatar.variants, dict):
# Return first available variant
for variant_url in self.avatar.variants.values():
if variant_url:
return variant_url
# Generate default letter-based avatar using first letter of username
first_letter = self.user.username[0].upper() if self.user.username else "U"
@@ -197,21 +215,32 @@ class UserProfile(models.Model):
"""
Return avatar variants for different use cases
"""
if self.avatar:
base_url = self.avatar.url
if '/public' in base_url:
return {
"thumbnail": base_url.replace('/public', '/thumbnail'),
"avatar": base_url.replace('/public', '/avatar'),
"large": base_url.replace('/public', '/large'),
}
else:
# If no variant in URL, return the same URL for all variants
return {
"thumbnail": base_url,
"avatar": base_url,
"large": base_url,
}
if self.avatar and self.avatar.is_uploaded:
variants = {}
# Try to get specific variants
thumbnail_url = self.avatar.get_url('thumbnail')
avatar_url = self.avatar.get_url('avatar')
large_url = self.avatar.get_url('large')
public_url = self.avatar.get_url('public')
# Use specific variants if available, otherwise fallback to public or first available
fallback_url = public_url
if not fallback_url and self.avatar.variants:
if isinstance(self.avatar.variants, list) and self.avatar.variants:
fallback_url = self.avatar.variants[0]
elif isinstance(self.avatar.variants, dict):
fallback_url = next(iter(self.avatar.variants.values()), None)
variants = {
"thumbnail": thumbnail_url or fallback_url,
"avatar": avatar_url or fallback_url,
"large": large_url or fallback_url,
}
# Only return variants if we have at least one valid URL
if any(variants.values()):
return variants
# For default avatars, return the same URL for all variants
default_url = self.get_avatar_url()