feat: Implement Phase 1.5 entity models (Park, Ride, Company, RideModel, Photo)

- 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
This commit is contained in:
pacnpal
2025-11-08 11:43:27 -05:00
parent 543d7bc9dc
commit 9c46ef8b03
17 changed files with 2326 additions and 0 deletions

View File

@@ -0,0 +1,266 @@
"""
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())