""" Ride-specific media models for ThrillWiki. This module contains media models specific to rides domain. """ from typing import Any, Optional, List, cast from django.db import models from django.conf import settings from apps.core.history import TrackedModel from apps.core.services.media_service import MediaService import pghistory def ride_photo_upload_path(instance: models.Model, filename: str) -> str: """Generate upload path for ride photos.""" photo = cast("RidePhoto", instance) ride = photo.ride if ride is None: raise ValueError("Ride cannot be None") return MediaService.generate_upload_path( domain="park", identifier=ride.slug, filename=filename, subdirectory=ride.park.slug, ) @pghistory.track() class RidePhoto(TrackedModel): """Photo model specific to rides.""" ride = models.ForeignKey( "rides.Ride", on_delete=models.CASCADE, related_name="photos" ) image = models.ImageField( upload_to=ride_photo_upload_path, max_length=255, ) caption = models.CharField(max_length=255, blank=True) alt_text = models.CharField(max_length=255, blank=True) is_primary = models.BooleanField(default=False) is_approved = models.BooleanField(default=False) # Ride-specific metadata photo_type = models.CharField( max_length=50, choices=[ ("exterior", "Exterior View"), ("queue", "Queue Area"), ("station", "Station"), ("onride", "On-Ride"), ("construction", "Construction"), ("other", "Other"), ], default="exterior", ) # Metadata created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) date_taken = models.DateTimeField(null=True, blank=True) # User who uploaded the photo uploaded_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, related_name="uploaded_ride_photos", ) class Meta: app_label = "rides" ordering = ["-is_primary", "-created_at"] indexes = [ models.Index(fields=["ride", "is_primary"]), models.Index(fields=["ride", "is_approved"]), models.Index(fields=["ride", "photo_type"]), models.Index(fields=["created_at"]), ] constraints = [ # Only one primary photo per ride models.UniqueConstraint( fields=["ride"], condition=models.Q(is_primary=True), name="unique_primary_ride_photo", ) ] def __str__(self) -> str: return f"Photo of {self.ride.name} - {self.caption or 'No caption'}" def save(self, *args: Any, **kwargs: Any) -> None: # Extract EXIF date if this is a new photo if not self.pk and not self.date_taken and self.image: self.date_taken = MediaService.extract_exif_date(self.image) # Set default caption if not provided if not self.caption and self.uploaded_by: self.caption = MediaService.generate_default_caption( self.uploaded_by.username ) # If this is marked as primary, unmark other primary photos for this ride if self.is_primary: RidePhoto.objects.filter( ride=self.ride, is_primary=True, ).exclude(pk=self.pk).update(is_primary=False) super().save(*args, **kwargs) @property def file_size(self) -> Optional[int]: """Get file size in bytes.""" try: return self.image.size except (ValueError, OSError): return None @property def dimensions(self) -> Optional[List[int]]: """Get image dimensions as [width, height].""" try: return [self.image.width, self.image.height] except (ValueError, OSError): return None def get_absolute_url(self) -> str: """Get absolute URL for this photo.""" return f"/parks/{self.ride.park.slug}/rides/{self.ride.slug}/photos/{self.pk}/" @property def park(self): """Get the park this ride belongs to.""" return self.ride.park