Files
2025-09-21 20:19:12 -04:00

306 lines
8.7 KiB
Python

"""
Ride-specific media service for ThrillWiki.
This module provides media management functionality specific to rides.
"""
import logging
from typing import List, Optional, Dict, Any
from django.core.files.uploadedfile import UploadedFile
from django.db import transaction
from django.contrib.auth import get_user_model
from apps.core.services.media_service import MediaService
from ..models import Ride, RidePhoto
User = get_user_model()
logger = logging.getLogger(__name__)
class RideMediaService:
"""Service for managing ride-specific media operations."""
@staticmethod
def upload_photo(
ride: Ride,
image_file: UploadedFile,
user: User,
caption: str = "",
alt_text: str = "",
photo_type: str = "exterior",
is_primary: bool = False,
auto_approve: bool = False,
) -> RidePhoto:
"""
Upload a photo for a ride.
Args:
ride: Ride instance
image_file: Uploaded image file
user: User uploading the photo
caption: Photo caption
alt_text: Alt text for accessibility
photo_type: Type of photo (exterior, queue, station, etc.)
is_primary: Whether this should be the primary photo
auto_approve: Whether to auto-approve the photo
Returns:
Created RidePhoto instance
Raises:
ValueError: If image validation fails
"""
# Validate image file
is_valid, error_message = MediaService.validate_image_file(image_file)
if not is_valid:
raise ValueError(error_message)
# Process image
processed_image = MediaService.process_image(image_file)
with transaction.atomic():
# Create photo instance
photo = RidePhoto(
ride=ride,
image=processed_image,
caption=caption or MediaService.generate_default_caption(user.username),
alt_text=alt_text,
photo_type=photo_type,
is_primary=is_primary,
is_approved=auto_approve,
uploaded_by=user,
)
# Extract EXIF date
photo.date_taken = MediaService.extract_exif_date(processed_image)
photo.save()
logger.info(f"Photo uploaded for ride {ride.slug} by user {user.username}")
return photo
@staticmethod
def get_ride_photos(
ride: Ride,
approved_only: bool = True,
primary_first: bool = True,
photo_type: Optional[str] = None,
) -> List[RidePhoto]:
"""
Get photos for a ride.
Args:
ride: Ride instance
approved_only: Whether to only return approved photos
primary_first: Whether to order primary photos first
photo_type: Filter by photo type (optional)
Returns:
List of RidePhoto instances
"""
queryset = ride.photos.all()
if approved_only:
queryset = queryset.filter(is_approved=True)
if photo_type:
queryset = queryset.filter(photo_type=photo_type)
if primary_first:
queryset = queryset.order_by("-is_primary", "-created_at")
else:
queryset = queryset.order_by("-created_at")
return list(queryset)
@staticmethod
def get_primary_photo(ride: Ride) -> Optional[RidePhoto]:
"""
Get the primary photo for a ride.
Args:
ride: Ride instance
Returns:
Primary RidePhoto instance or None
"""
try:
return ride.photos.filter(is_primary=True, is_approved=True).first()
except RidePhoto.DoesNotExist:
return None
@staticmethod
def get_photos_by_type(ride: Ride, photo_type: str) -> List[RidePhoto]:
"""
Get photos of a specific type for a ride.
Args:
ride: Ride instance
photo_type: Type of photos to retrieve
Returns:
List of RidePhoto instances
"""
return list(
ride.photos.filter(photo_type=photo_type, is_approved=True).order_by(
"-created_at"
)
)
@staticmethod
def set_primary_photo(ride: Ride, photo: RidePhoto) -> bool:
"""
Set a photo as the primary photo for a ride.
Args:
ride: Ride instance
photo: RidePhoto to set as primary
Returns:
True if successful, False otherwise
"""
if photo.ride != ride:
return False
with transaction.atomic():
# Unset current primary
ride.photos.filter(is_primary=True).update(is_primary=False)
# Set new primary
photo.is_primary = True
photo.save()
logger.info(f"Set photo {photo.pk} as primary for ride {ride.slug}")
return True
@staticmethod
def approve_photo(photo: RidePhoto, approved_by: User) -> bool:
"""
Approve a ride photo.
Args:
photo: RidePhoto to approve
approved_by: User approving the photo
Returns:
True if successful, False otherwise
"""
try:
photo.is_approved = True
photo.save()
logger.info(f"Photo {photo.pk} approved by user {approved_by.username}")
return True
except Exception as e:
logger.error(f"Failed to approve photo {photo.pk}: {str(e)}")
return False
@staticmethod
def delete_photo(photo: RidePhoto, deleted_by: User) -> bool:
"""
Delete a ride photo.
Args:
photo: RidePhoto to delete
deleted_by: User deleting the photo
Returns:
True if successful, False otherwise
"""
try:
ride_slug = photo.ride.slug
photo_id = photo.pk
# Delete the file and database record
if photo.image:
photo.image.delete(save=False)
photo.delete()
logger.info(
f"Photo {photo_id} deleted from ride {ride_slug} by user {deleted_by.username}"
)
return True
except Exception as e:
logger.error(f"Failed to delete photo {photo.pk}: {str(e)}")
return False
@staticmethod
def get_photo_stats(ride: Ride) -> Dict[str, Any]:
"""
Get photo statistics for a ride.
Args:
ride: Ride instance
Returns:
Dictionary with photo statistics
"""
photos = ride.photos.all()
# Get counts by photo type
type_counts = {}
for photo_type, _ in RidePhoto._meta.get_field("photo_type").choices:
type_counts[photo_type] = photos.filter(photo_type=photo_type).count()
return {
"total_photos": photos.count(),
"approved_photos": photos.filter(is_approved=True).count(),
"pending_photos": photos.filter(is_approved=False).count(),
"has_primary": photos.filter(is_primary=True).exists(),
"recent_uploads": photos.order_by("-created_at")[:5].count(),
"by_type": type_counts,
}
@staticmethod
def bulk_approve_photos(photos: List[RidePhoto], approved_by: User) -> int:
"""
Bulk approve multiple photos.
Args:
photos: List of RidePhoto instances to approve
approved_by: User approving the photos
Returns:
Number of photos successfully approved
"""
approved_count = 0
with transaction.atomic():
for photo in photos:
if RideMediaService.approve_photo(photo, approved_by):
approved_count += 1
logger.info(
f"Bulk approved {approved_count} photos by user {approved_by.username}"
)
return approved_count
@staticmethod
def get_construction_timeline(ride: Ride) -> List[RidePhoto]:
"""
Get construction photos ordered chronologically.
Args:
ride: Ride instance
Returns:
List of construction RidePhoto instances ordered by date taken
"""
return list(
ride.photos.filter(photo_type="construction", is_approved=True).order_by(
"date_taken", "created_at"
)
)
@staticmethod
def get_onride_photos(ride: Ride) -> List[RidePhoto]:
"""
Get on-ride photos for a ride.
Args:
ride: Ride instance
Returns:
List of on-ride RidePhoto instances
"""
return RideMediaService.get_photos_by_type(ride, "onride")