""" Media models for ThrillWiki Django backend. This module contains models for handling media content: - Photo: CloudFlare Images integration with generic relations """ from django.db import models from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType from django_lifecycle import hook, AFTER_CREATE, AFTER_UPDATE, BEFORE_SAVE from apps.core.models import BaseModel class Photo(BaseModel): """ Represents a photo stored in CloudFlare Images. Uses generic relations to attach to any entity (Park, Ride, Company, etc.) """ PHOTO_TYPE_CHOICES = [ ('main', 'Main Photo'), ('gallery', 'Gallery Photo'), ('banner', 'Banner Image'), ('logo', 'Logo'), ('thumbnail', 'Thumbnail'), ('other', 'Other'), ] MODERATION_STATUS_CHOICES = [ ('pending', 'Pending Review'), ('approved', 'Approved'), ('rejected', 'Rejected'), ('flagged', 'Flagged'), ] # CloudFlare Image Integration cloudflare_image_id = models.CharField( max_length=255, unique=True, db_index=True, help_text="Unique CloudFlare image identifier" ) cloudflare_url = models.URLField( help_text="CloudFlare CDN URL for the image" ) cloudflare_thumbnail_url = models.URLField( blank=True, help_text="CloudFlare thumbnail URL (if different from main URL)" ) # Metadata title = models.CharField( max_length=255, blank=True, help_text="Photo title or caption" ) description = models.TextField( blank=True, help_text="Photo description or details" ) credit = models.CharField( max_length=255, blank=True, help_text="Photo credit/photographer name" ) # Photo Type photo_type = models.CharField( max_length=50, choices=PHOTO_TYPE_CHOICES, default='gallery', db_index=True, help_text="Type of photo" ) # Generic relation to attach to any entity content_type = models.ForeignKey( ContentType, on_delete=models.CASCADE, help_text="Type of entity this photo belongs to" ) object_id = models.UUIDField( db_index=True, help_text="ID of the entity this photo belongs to" ) content_object = GenericForeignKey('content_type', 'object_id') # User who uploaded uploaded_by = models.ForeignKey( 'users.User', on_delete=models.SET_NULL, null=True, blank=True, related_name='uploaded_photos', help_text="User who uploaded this photo" ) # Moderation moderation_status = models.CharField( max_length=50, choices=MODERATION_STATUS_CHOICES, default='pending', db_index=True, help_text="Moderation status" ) is_approved = models.BooleanField( default=False, db_index=True, help_text="Quick filter for approved photos" ) moderated_by = models.ForeignKey( 'users.User', on_delete=models.SET_NULL, null=True, blank=True, related_name='moderated_photos', help_text="Moderator who approved/rejected this photo" ) moderated_at = models.DateTimeField( null=True, blank=True, help_text="When the photo was moderated" ) moderation_notes = models.TextField( blank=True, help_text="Notes from moderator" ) # Image Metadata width = models.IntegerField( null=True, blank=True, help_text="Image width in pixels" ) height = models.IntegerField( null=True, blank=True, help_text="Image height in pixels" ) file_size = models.IntegerField( null=True, blank=True, help_text="File size in bytes" ) # Display Order display_order = models.IntegerField( default=0, db_index=True, help_text="Order for displaying in galleries (lower numbers first)" ) # Visibility is_featured = models.BooleanField( default=False, db_index=True, help_text="Is this a featured photo?" ) is_public = models.BooleanField( default=True, db_index=True, help_text="Is this photo publicly visible?" ) class Meta: verbose_name = 'Photo' verbose_name_plural = 'Photos' ordering = ['display_order', '-created'] indexes = [ models.Index(fields=['content_type', 'object_id']), models.Index(fields=['cloudflare_image_id']), models.Index(fields=['moderation_status']), models.Index(fields=['is_approved']), models.Index(fields=['uploaded_by']), models.Index(fields=['photo_type']), models.Index(fields=['display_order']), ] def __str__(self): if self.title: return self.title return f"Photo {self.cloudflare_image_id[:8]}..." @hook(AFTER_UPDATE, when='moderation_status', was='pending', is_now='approved') def set_approved_flag_on_approval(self): """Set is_approved flag when status changes to approved.""" self.is_approved = True self.save(update_fields=['is_approved']) @hook(AFTER_UPDATE, when='moderation_status', was='approved', is_not='approved') def clear_approved_flag_on_rejection(self): """Clear is_approved flag when status changes from approved.""" self.is_approved = False self.save(update_fields=['is_approved']) def approve(self, moderator, notes=''): """Approve this photo.""" from django.utils import timezone self.moderation_status = 'approved' self.is_approved = True self.moderated_by = moderator self.moderated_at = timezone.now() self.moderation_notes = notes self.save(update_fields=[ 'moderation_status', 'is_approved', 'moderated_by', 'moderated_at', 'moderation_notes' ]) def reject(self, moderator, notes=''): """Reject this photo.""" from django.utils import timezone self.moderation_status = 'rejected' self.is_approved = False self.moderated_by = moderator self.moderated_at = timezone.now() self.moderation_notes = notes self.save(update_fields=[ 'moderation_status', 'is_approved', 'moderated_by', 'moderated_at', 'moderation_notes' ]) def flag(self, moderator, notes=''): """Flag this photo for review.""" from django.utils import timezone self.moderation_status = 'flagged' self.is_approved = False self.moderated_by = moderator self.moderated_at = timezone.now() self.moderation_notes = notes self.save(update_fields=[ 'moderation_status', 'is_approved', 'moderated_by', 'moderated_at', 'moderation_notes' ]) class PhotoManager(models.Manager): """Custom manager for Photo model.""" def approved(self): """Return only approved photos.""" return self.filter(is_approved=True) def pending(self): """Return only pending photos.""" return self.filter(moderation_status='pending') def public(self): """Return only public, approved photos.""" return self.filter(is_approved=True, is_public=True) # Add custom manager to Photo model Photo.add_to_class('objects', PhotoManager())