mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 07:31:07 -05:00
- Created ParkPhoto and ParkPhotoEvent models in the parks app, including fields for image, caption, alt text, and relationships to the Park model. - Implemented triggers for insert and update operations on ParkPhoto to log changes in ParkPhotoEvent. - Created RidePhoto and RidePhotoEvent models in the rides app, with similar structure and functionality as ParkPhoto. - Added fields for photo type in RidePhoto and implemented corresponding triggers for logging changes. - Established necessary indexes and unique constraints for both models to ensure data integrity and optimize queries.
306 lines
8.7 KiB
Python
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")
|