""" 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')