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

242 lines
6.8 KiB
Python

"""
Park-specific media service for ThrillWiki.
This module provides media management functionality specific to parks.
"""
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 Park, ParkPhoto
User = get_user_model()
logger = logging.getLogger(__name__)
class ParkMediaService:
"""Service for managing park-specific media operations."""
@staticmethod
def upload_photo(
park: Park,
image_file: UploadedFile,
user: User,
caption: str = "",
alt_text: str = "",
is_primary: bool = False,
auto_approve: bool = False,
) -> ParkPhoto:
"""
Upload a photo for a park.
Args:
park: Park instance
image_file: Uploaded image file
user: User uploading the photo
caption: Photo caption
alt_text: Alt text for accessibility
is_primary: Whether this should be the primary photo
auto_approve: Whether to auto-approve the photo
Returns:
Created ParkPhoto 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 = ParkPhoto(
park=park,
image=processed_image,
caption=caption or MediaService.generate_default_caption(user.username),
alt_text=alt_text,
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 park {park.slug} by user {user.username}")
return photo
@staticmethod
def get_park_photos(
park: Park, approved_only: bool = True, primary_first: bool = True
) -> List[ParkPhoto]:
"""
Get photos for a park.
Args:
park: Park instance
approved_only: Whether to only return approved photos
primary_first: Whether to order primary photos first
Returns:
List of ParkPhoto instances
"""
queryset = park.photos.all()
if approved_only:
queryset = queryset.filter(is_approved=True)
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(park: Park) -> Optional[ParkPhoto]:
"""
Get the primary photo for a park.
Args:
park: Park instance
Returns:
Primary ParkPhoto instance or None
"""
try:
return park.photos.filter(is_primary=True, is_approved=True).first()
except ParkPhoto.DoesNotExist:
return None
@staticmethod
def set_primary_photo(park: Park, photo: ParkPhoto) -> bool:
"""
Set a photo as the primary photo for a park.
Args:
park: Park instance
photo: ParkPhoto to set as primary
Returns:
True if successful, False otherwise
"""
if photo.park != park:
return False
with transaction.atomic():
# Unset current primary
park.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 park {park.slug}")
return True
@staticmethod
def approve_photo(photo: ParkPhoto, approved_by: User) -> bool:
"""
Approve a park photo.
Args:
photo: ParkPhoto 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: ParkPhoto, deleted_by: User) -> bool:
"""
Delete a park photo.
Args:
photo: ParkPhoto to delete
deleted_by: User deleting the photo
Returns:
True if successful, False otherwise
"""
try:
park_slug = photo.park.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 park {park_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(park: Park) -> Dict[str, Any]:
"""
Get photo statistics for a park.
Args:
park: Park instance
Returns:
Dictionary with photo statistics
"""
photos = park.photos.all()
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(),
}
@staticmethod
def bulk_approve_photos(photos: List[ParkPhoto], approved_by: User) -> int:
"""
Bulk approve multiple photos.
Args:
photos: List of ParkPhoto 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 ParkMediaService.approve_photo(photo, approved_by):
approved_count += 1
logger.info(
f"Bulk approved {approved_count} photos by user {approved_by.username}"
)
return approved_count