Files
thrillwiki_django_no_react/backend/apps/parks/services/media_service.py

245 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 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 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) -> ParkPhoto | None:
"""
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