from typing import Any, Optional, Union, cast from django.db import models from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType from django.utils.text import slugify from django.conf import settings import os from .storage import MediaStorage from rides.models import Ride from django.utils import timezone def photo_upload_path(instance: models.Model, filename: str) -> str: """Generate upload path for photos using normalized filenames""" # Get the content type and object photo = cast(Photo, instance) content_type = photo.content_type.model obj = photo.content_object if obj is None: raise ValueError("Content object cannot be None") # Get object identifier (slug or id) identifier = getattr(obj, 'slug', None) if identifier is None: identifier = obj.pk # Use pk instead of id as it's guaranteed to exist # Get the next available number for this object existing_photos = Photo.objects.filter( content_type=photo.content_type, object_id=photo.object_id ).count() next_number = existing_photos + 1 # Create normalized filename ext = os.path.splitext(filename)[1].lower() or '.jpg' # Default to .jpg if no extension new_filename = f"{identifier}_{next_number}{ext}" # If it's a ride photo, store it under the park's directory if content_type == 'ride': ride = cast(Ride, obj) return f"park/{ride.park.slug}/{identifier}/{new_filename}" # For park photos, store directly in park directory return f"park/{identifier}/{new_filename}" class Photo(models.Model): """Generic photo model that can be attached to any model""" image = models.ImageField( upload_to=photo_upload_path, # type: ignore[arg-type] max_length=255, storage=MediaStorage() ) 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) # New field for approval status created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) uploaded_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, related_name='uploaded_photos' ) # Generic foreign key fields content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) object_id = models.PositiveIntegerField() content_object = GenericForeignKey('content_type', 'object_id') class Meta: ordering = ['-is_primary', '-created_at'] indexes = [ models.Index(fields=['content_type', 'object_id']), ] def __str__(self) -> str: return f"{self.content_type} - {self.content_object} - {self.caption or 'No caption'}" def save(self, *args: Any, **kwargs: Any) -> None: # Set default caption if not provided if not self.caption and self.uploaded_by: current_time = timezone.now() self.caption = f"Uploaded by {self.uploaded_by.username} on {current_time.strftime('%B %d, %Y at %I:%M %p')}" # If this is marked as primary, unmark other primary photos if self.is_primary: Photo.objects.filter( content_type=self.content_type, object_id=self.object_id, is_primary=True ).exclude(pk=self.pk).update(is_primary=False) # Use pk instead of id super().save(*args, **kwargs)