mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 11:11:16 -05:00
- Created Company model with location tracking, date precision, and CloudFlare Images - Created RideModel model for manufacturer's ride models with specifications - Created Park model with location, dates, operator, and cached statistics - Created Ride model with comprehensive stats, manufacturer, and park relationship - Created Photo model with CloudFlare Images integration and generic relations - Added lifecycle hooks for auto-slug generation and count updates - Created migrations and applied to database - Registered all models in Django admin with detailed fieldsets - Fixed admin autocomplete_fields to use raw_id_fields where needed - All models inherit from VersionedModel for automatic version tracking - Models include date precision tracking for opening/closing dates - Added comprehensive indexes for query performance Phase 1.5 complete - Entity models ready for API development
267 lines
7.7 KiB
Python
267 lines
7.7 KiB
Python
"""
|
|
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())
|