""" Ride-specific media service for ThrillWiki. This module provides media management functionality specific to rides. """ import logging from typing import Any from django.contrib.auth import get_user_model from django.core.files.uploadedfile import UploadedFile from django.db import transaction 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: str | None = 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) -> RidePhoto | None: """ 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")