Files
pacnpal 9bed782784 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.
2025-08-30 21:20:25 -04:00

121 lines
3.7 KiB
Python

"""
Park-specific media models for ThrillWiki.
This module contains media models specific to parks domain.
"""
from typing import Any, List, Optional, cast
from django.db import models
from django.conf import settings
from apps.core.history import TrackedModel
from apps.core.services.media_service import MediaService
import pghistory
def park_photo_upload_path(instance: models.Model, filename: str) -> str:
"""Generate upload path for park photos."""
photo = cast("ParkPhoto", instance)
park = photo.park
if park is None:
raise ValueError("Park cannot be None")
return MediaService.generate_upload_path(
domain="park", identifier=park.slug, filename=filename
)
@pghistory.track()
class ParkPhoto(TrackedModel):
"""Photo model specific to parks."""
park = models.ForeignKey(
"parks.Park", on_delete=models.CASCADE, related_name="photos"
)
image = models.ForeignKey(
'django_cloudflareimages_toolkit.CloudflareImage',
on_delete=models.CASCADE,
help_text="Park photo stored on Cloudflare Images"
)
caption = models.CharField(max_length=255, blank=True)
alt_text = models.CharField(max_length=255, blank=True)
is_primary = models.BooleanField(default=False)
is_approved = models.BooleanField(default=False)
# Metadata
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
date_taken = models.DateTimeField(null=True, blank=True)
# User who uploaded the photo
uploaded_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
related_name="uploaded_park_photos",
)
class Meta(TrackedModel.Meta):
app_label = "parks"
ordering = ["-is_primary", "-created_at"]
indexes = [
models.Index(fields=["park", "is_primary"]),
models.Index(fields=["park", "is_approved"]),
models.Index(fields=["created_at"]),
]
constraints = [
# Only one primary photo per park
models.UniqueConstraint(
fields=["park"],
condition=models.Q(is_primary=True),
name="unique_primary_park_photo",
)
]
def __str__(self) -> str:
return f"Photo of {self.park.name} - {self.caption or 'No caption'}"
def save(self, *args: Any, **kwargs: Any) -> None:
# Extract EXIF date if this is a new photo
if not self.pk and not self.date_taken and self.image:
self.date_taken = MediaService.extract_exif_date(self.image)
# Set default caption if not provided
if not self.caption and self.uploaded_by:
self.caption = MediaService.generate_default_caption(
self.uploaded_by.username
)
# If this is marked as primary, unmark other primary photos for this park
if self.is_primary:
ParkPhoto.objects.filter(
park=self.park,
is_primary=True,
).exclude(
pk=self.pk
).update(is_primary=False)
super().save(*args, **kwargs)
@property
def file_size(self) -> Optional[int]:
"""Get file size in bytes."""
try:
return self.image.size
except (ValueError, OSError):
return None
@property
def dimensions(self) -> Optional[List[int]]:
"""Get image dimensions as [width, height]."""
try:
return [self.image.width, self.image.height]
except (ValueError, OSError):
return None
def get_absolute_url(self) -> str:
"""Get absolute URL for this photo."""
return f"/parks/{self.park.slug}/photos/{self.pk}/"