- Added migration 0011 to populate unique slugs for existing RideModel records based on manufacturer and model names. - Implemented logic to ensure slug uniqueness during population. - Added reverse migration to clear slugs if needed. feat(rides): enforce unique slugs for RideModel - Created migration 0012 to alter the slug field in RideModel to be unique. - Updated the slug field to include help text and a maximum length of 255 characters. docs: integrate Cloudflare Images into rides and parks models - Updated RidePhoto and ParkPhoto models to use CloudflareImagesField for image storage. - Enhanced API serializers for rides and parks to support Cloudflare Images, including new fields for image URLs and variants. - Provided comprehensive OpenAPI schema metadata for new fields. - Documented database migrations for the integration. - Detailed configuration settings for Cloudflare Images. - Updated API response formats to include Cloudflare Images URLs and variants. - Added examples for uploading photos via API and outlined testing procedures.
19 KiB
Cloudflare Images Integration
Overview
This document describes the complete integration of django-cloudflare-images into the ThrillWiki project for both rides and parks models, including full API schema metadata support.
Implementation Summary
1. Models Updated
Rides Models (backend/apps/rides/models/media.py)
- RidePhoto.image: Changed from
models.ImageFieldtoCloudflareImagesField(variant="public") - Added proper Meta class inheritance from
TrackedModel.Meta - Maintains all existing functionality while leveraging Cloudflare Images
Parks Models (backend/apps/parks/models/media.py)
- ParkPhoto.image: Changed from
models.ImageFieldtoCloudflareImagesField(variant="public") - Added proper Meta class inheritance from
TrackedModel.Meta - Maintains all existing functionality while leveraging Cloudflare Images
2. API Serializers Enhanced
Rides API (backend/apps/api/v1/rides/serializers.py)
- RidePhotoOutputSerializer: Enhanced with Cloudflare Images support
- Added
image_urlfield: Full URL to the Cloudflare Images asset - Added
image_variantsfield: Dictionary of available image variants with URLs - Proper DRF Spectacular schema decorations with examples
- Maintains backward compatibility
- Added
Parks API (backend/apps/api/v1/parks/serializers.py)
- ParkPhotoOutputSerializer: Enhanced with Cloudflare Images support
- Added
image_urlfield: Full URL to the Cloudflare Images asset - Added
image_variantsfield: Dictionary of available image variants with URLs - Proper DRF Spectacular schema decorations with examples
- Maintains backward compatibility
- Added
3. Schema Metadata
Both serializers include comprehensive OpenAPI schema metadata:
- Field Documentation: All new fields have detailed help text and type information
- Examples: Complete example responses showing Cloudflare Images URLs and variants
- Variants: Documented image variants (thumbnail, medium, large, public) with descriptions
4. Database Migrations
- rides.0008_cloudflare_images_integration: Updates RidePhoto.image field
- parks.0009_cloudflare_images_integration: Updates ParkPhoto.image field
- Migrations applied successfully with no data loss
Configuration
The project already has Cloudflare Images configured in backend/config/django/base.py:
# Cloudflare Images Settings
STORAGES = {
"default": {
"BACKEND": "cloudflare_images.storage.CloudflareImagesStorage",
},
# ... other storage configs
}
CLOUDFLARE_IMAGES_ACCOUNT_ID = config("CLOUDFLARE_IMAGES_ACCOUNT_ID")
CLOUDFLARE_IMAGES_API_TOKEN = config("CLOUDFLARE_IMAGES_API_TOKEN")
CLOUDFLARE_IMAGES_ACCOUNT_HASH = config("CLOUDFLARE_IMAGES_ACCOUNT_HASH")
CLOUDFLARE_IMAGES_DOMAIN = config("CLOUDFLARE_IMAGES_DOMAIN", default="imagedelivery.net")
API Response Format
Enhanced Photo Response
Both ride and park photo endpoints now return:
{
"id": 123,
"image": "https://imagedelivery.net/account-hash/image-id/public",
"image_url": "https://imagedelivery.net/account-hash/image-id/public",
"image_variants": {
"thumbnail": "https://imagedelivery.net/account-hash/image-id/thumbnail",
"medium": "https://imagedelivery.net/account-hash/image-id/medium",
"large": "https://imagedelivery.net/account-hash/image-id/large",
"public": "https://imagedelivery.net/account-hash/image-id/public"
},
"caption": "Photo caption",
"alt_text": "Alt text for accessibility",
"is_primary": true,
"is_approved": true,
"photo_type": "exterior", // rides only
"created_at": "2023-01-01T12:00:00Z",
"updated_at": "2023-01-01T12:00:00Z",
"date_taken": "2023-01-01T10:00:00Z",
"uploaded_by_username": "photographer123",
"file_size": 2048576,
"dimensions": [1920, 1080],
"ride_slug": "steel-vengeance", // rides only
"ride_name": "Steel Vengeance", // rides only
"park_slug": "cedar-point",
"park_name": "Cedar Point"
}
Image Variants
The integration provides these standard variants:
- thumbnail: 150x150px - Perfect for list views and previews
- medium: 500x500px - Good for modal previews and medium displays
- large: 1200x1200px - High quality for detailed views
- public: Original size - Full resolution image
Benefits
- Performance: Cloudflare's global CDN ensures fast image delivery
- Optimization: Automatic image optimization and format conversion
- Variants: Multiple image sizes generated automatically
- Scalability: No local storage requirements
- API Documentation: Complete OpenAPI schema with examples
- Backward Compatibility: Existing API consumers continue to work
- Entity Validation: Photos are always associated with valid rides or parks
- Data Integrity: Prevents orphaned photos without parent entities
- Automatic Photo Inclusion: Photos are automatically included when displaying rides and parks
- Primary Photo Support: Easy access to the main photo for each entity
Automatic Photo Integration
Ride Detail Responses
When fetching ride details via GET /api/v1/rides/{id}/, the response automatically includes:
- photos: Array of up to 10 approved photos with full Cloudflare Images variants
- primary_photo: The designated primary photo for the ride (if available)
{
"id": 1,
"name": "Steel Vengeance",
"slug": "steel-vengeance",
"photos": [
{
"id": 123,
"image_url": "https://imagedelivery.net/account-hash/abc123def456/public",
"image_variants": {
"thumbnail": "https://imagedelivery.net/account-hash/abc123def456/thumbnail",
"medium": "https://imagedelivery.net/account-hash/abc123def456/medium",
"large": "https://imagedelivery.net/account-hash/abc123def456/large",
"public": "https://imagedelivery.net/account-hash/abc123def456/public"
},
"caption": "Amazing roller coaster photo",
"alt_text": "Steel roller coaster with multiple inversions",
"is_primary": true,
"photo_type": "exterior"
}
],
"primary_photo": {
"id": 123,
"image_url": "https://imagedelivery.net/account-hash/abc123def456/public",
"image_variants": {
"thumbnail": "https://imagedelivery.net/account-hash/abc123def456/thumbnail",
"medium": "https://imagedelivery.net/account-hash/abc123def456/medium",
"large": "https://imagedelivery.net/account-hash/abc123def456/large",
"public": "https://imagedelivery.net/account-hash/abc123def456/public"
},
"caption": "Amazing roller coaster photo",
"alt_text": "Steel roller coaster with multiple inversions",
"photo_type": "exterior"
}
}
Park Detail Responses
When fetching park details via GET /api/v1/parks/{id}/, the response automatically includes:
- photos: Array of up to 10 approved photos with full Cloudflare Images variants
- primary_photo: The designated primary photo for the park (if available)
{
"id": 1,
"name": "Cedar Point",
"slug": "cedar-point",
"photos": [
{
"id": 456,
"image_url": "https://imagedelivery.net/account-hash/def789ghi012/public",
"image_variants": {
"thumbnail": "https://imagedelivery.net/account-hash/def789ghi012/thumbnail",
"medium": "https://imagedelivery.net/account-hash/def789ghi012/medium",
"large": "https://imagedelivery.net/account-hash/def789ghi012/large",
"public": "https://imagedelivery.net/account-hash/def789ghi012/public"
},
"caption": "Beautiful park entrance",
"alt_text": "Cedar Point main entrance with flags",
"is_primary": true
}
],
"primary_photo": {
"id": 456,
"image_url": "https://imagedelivery.net/account-hash/def789ghi012/public",
"image_variants": {
"thumbnail": "https://imagedelivery.net/account-hash/def789ghi012/thumbnail",
"medium": "https://imagedelivery.net/account-hash/def789ghi012/medium",
"large": "https://imagedelivery.net/account-hash/def789ghi012/large",
"public": "https://imagedelivery.net/account-hash/def789ghi012/public"
},
"caption": "Beautiful park entrance",
"alt_text": "Cedar Point main entrance with flags"
}
}
Photo Filtering
- Only approved photos (
is_approved=True) are included in entity responses - Photos are ordered by primary status first, then by creation date (newest first)
- Limited to 10 photos maximum per entity to maintain response performance
- Primary photo is provided separately for easy access to the main image
Testing
The implementation has been verified:
- ✅ Models successfully use CloudflareImagesField
- ✅ Migrations applied without issues
- ✅ Serializers import and function correctly
- ✅ Schema metadata properly configured
- ✅ Photos automatically included in ride and park detail responses
- ✅ Primary photo selection working correctly
Upload Examples
1. Upload Ride Photo via API
Endpoint: POST /api/v1/rides/{ride_id}/photos/
Requirements:
- Valid JWT authentication token
- Existing ride with the specified
ride_id - Image file in supported format (JPEG, PNG, WebP, etc.)
Headers:
Authorization: Bearer <your_jwt_token>
Content-Type: multipart/form-data
cURL Example:
curl -X POST "https://your-domain.com/api/v1/rides/123/photos/" \
-H "Authorization: Bearer your_jwt_token_here" \
-F "image=@/path/to/your/photo.jpg" \
-F "caption=Amazing steel coaster shot" \
-F "alt_text=Steel Vengeance coaster with riders" \
-F "photo_type=exterior" \
-F "is_primary=false"
Error Response (Non-existent Ride):
{
"detail": "Ride not found"
}
Python Example:
import requests
url = "https://your-domain.com/api/v1/rides/123/photos/"
headers = {"Authorization": "Bearer your_jwt_token_here"}
with open("/path/to/your/photo.jpg", "rb") as image_file:
files = {"image": image_file}
data = {
"caption": "Amazing steel coaster shot",
"alt_text": "Steel Vengeance coaster with riders",
"photo_type": "exterior",
"is_primary": False
}
response = requests.post(url, headers=headers, files=files, data=data)
print(response.json())
JavaScript Example:
const formData = new FormData();
formData.append('image', fileInput.files[0]);
formData.append('caption', 'Amazing steel coaster shot');
formData.append('alt_text', 'Steel Vengeance coaster with riders');
formData.append('photo_type', 'exterior');
formData.append('is_primary', 'false');
fetch('/api/v1/rides/123/photos/', {
method: 'POST',
headers: {
'Authorization': 'Bearer your_jwt_token_here'
},
body: formData
})
.then(response => response.json())
.then(data => console.log(data));
2. Upload Park Photo via API
Endpoint: POST /api/v1/parks/{park_id}/photos/
Requirements:
- Valid JWT authentication token
- Existing park with the specified
park_id - Image file in supported format (JPEG, PNG, WebP, etc.)
cURL Example:
curl -X POST "https://your-domain.com/api/v1/parks/456/photos/" \
-H "Authorization: Bearer your_jwt_token_here" \
-F "image=@/path/to/park-entrance.jpg" \
-F "caption=Beautiful park entrance" \
-F "alt_text=Cedar Point main entrance with flags" \
-F "is_primary=true"
Error Response (Non-existent Park):
{
"detail": "Park not found"
}
3. Upload Response Format
Both endpoints return the same enhanced format with Cloudflare Images integration:
{
"id": 789,
"image": "https://imagedelivery.net/account-hash/image-id/public",
"image_url": "https://imagedelivery.net/account-hash/image-id/public",
"image_variants": {
"thumbnail": "https://imagedelivery.net/account-hash/image-id/thumbnail",
"medium": "https://imagedelivery.net/account-hash/image-id/medium",
"large": "https://imagedelivery.net/account-hash/image-id/large",
"public": "https://imagedelivery.net/account-hash/image-id/public"
},
"caption": "Amazing steel coaster shot",
"alt_text": "Steel Vengeance coaster with riders",
"is_primary": false,
"is_approved": false,
"photo_type": "exterior",
"created_at": "2023-01-01T12:00:00Z",
"updated_at": "2023-01-01T12:00:00Z",
"date_taken": null,
"uploaded_by_username": "photographer123",
"file_size": 2048576,
"dimensions": [1920, 1080],
"ride_slug": "steel-vengeance",
"ride_name": "Steel Vengeance",
"park_slug": "cedar-point",
"park_name": "Cedar Point"
}
Cloudflare Images Transformations
1. Built-in Variants
The integration provides these pre-configured variants:
- thumbnail (150x150px):
https://imagedelivery.net/account-hash/image-id/thumbnail - medium (500x500px):
https://imagedelivery.net/account-hash/image-id/medium - large (1200x1200px):
https://imagedelivery.net/account-hash/image-id/large - public (original):
https://imagedelivery.net/account-hash/image-id/public
2. Custom Transformations
You can apply custom transformations by appending parameters to any variant URL:
Resize Examples:
# Resize to specific width (maintains aspect ratio)
https://imagedelivery.net/account-hash/image-id/public/w=800
# Resize to specific height (maintains aspect ratio)
https://imagedelivery.net/account-hash/image-id/public/h=600
# Resize to exact dimensions (may crop)
https://imagedelivery.net/account-hash/image-id/public/w=800,h=600
# Resize with fit modes
https://imagedelivery.net/account-hash/image-id/public/w=800,h=600,fit=cover
https://imagedelivery.net/account-hash/image-id/public/w=800,h=600,fit=contain
https://imagedelivery.net/account-hash/image-id/public/w=800,h=600,fit=crop
Quality and Format:
# Adjust quality (1-100)
https://imagedelivery.net/account-hash/image-id/public/quality=85
# Convert format
https://imagedelivery.net/account-hash/image-id/public/format=webp
https://imagedelivery.net/account-hash/image-id/public/format=avif
# Auto format (serves best format for browser)
https://imagedelivery.net/account-hash/image-id/public/format=auto
Advanced Transformations:
# Blur effect
https://imagedelivery.net/account-hash/image-id/public/blur=5
# Sharpen
https://imagedelivery.net/account-hash/image-id/public/sharpen=2
# Brightness adjustment (-100 to 100)
https://imagedelivery.net/account-hash/image-id/public/brightness=20
# Contrast adjustment (-100 to 100)
https://imagedelivery.net/account-hash/image-id/public/contrast=15
# Gamma adjustment (0.1 to 2.0)
https://imagedelivery.net/account-hash/image-id/public/gamma=1.2
# Rotate (90, 180, 270 degrees)
https://imagedelivery.net/account-hash/image-id/public/rotate=90
Combining Transformations:
# Multiple transformations (comma-separated)
https://imagedelivery.net/account-hash/image-id/public/w=800,h=600,fit=cover,quality=85,format=webp
# Responsive image for mobile
https://imagedelivery.net/account-hash/image-id/public/w=400,quality=80,format=auto
# High-quality desktop version
https://imagedelivery.net/account-hash/image-id/public/w=1200,quality=90,format=auto
3. Creating Custom Variants
You can create custom variants in your Cloudflare Images dashboard for commonly used transformations:
- Go to Cloudflare Images dashboard
- Navigate to "Variants" section
- Create new variant with desired transformations
- Use in your models:
# In your model
class RidePhoto(TrackedModel):
image = CloudflareImagesField(variant="hero_banner") # Custom variant
4. Responsive Images Implementation
Use different variants for responsive design:
<!-- HTML with responsive variants -->
<picture>
<source media="(max-width: 480px)"
srcset="https://imagedelivery.net/account-hash/image-id/thumbnail">
<source media="(max-width: 768px)"
srcset="https://imagedelivery.net/account-hash/image-id/medium">
<source media="(max-width: 1200px)"
srcset="https://imagedelivery.net/account-hash/image-id/large">
<img src="https://imagedelivery.net/account-hash/image-id/public"
alt="Ride photo">
</picture>
/* CSS with responsive variants */
.ride-photo {
background-image: url('https://imagedelivery.net/account-hash/image-id/thumbnail');
}
@media (min-width: 768px) {
.ride-photo {
background-image: url('https://imagedelivery.net/account-hash/image-id/medium');
}
}
@media (min-width: 1200px) {
.ride-photo {
background-image: url('https://imagedelivery.net/account-hash/image-id/large');
}
}
5. Performance Optimization
Best Practices:
- Use
format=autoto serve optimal format (WebP, AVIF) based on browser support - Set appropriate quality levels (80-85 for photos, 90+ for graphics)
- Use
fit=coverfor consistent aspect ratios in galleries - Implement lazy loading with smaller variants as placeholders
Example Optimized URLs:
# Gallery thumbnail (fast loading)
https://imagedelivery.net/account-hash/image-id/thumbnail/quality=75,format=auto
# Modal preview (balanced quality/size)
https://imagedelivery.net/account-hash/image-id/medium/quality=85,format=auto
# Full-size view (high quality)
https://imagedelivery.net/account-hash/image-id/large/quality=90,format=auto
Testing and Verification
1. Verify Upload Functionality
# Test ride photo upload (requires existing ride with ID 1)
curl -X POST "http://localhost:8000/api/v1/rides/1/photos/" \
-H "Authorization: Bearer your_test_token" \
-F "image=@test_image.jpg" \
-F "caption=Test upload"
# Test park photo upload (requires existing park with ID 1)
curl -X POST "http://localhost:8000/api/v1/parks/1/photos/" \
-H "Authorization: Bearer your_test_token" \
-F "image=@test_image.jpg" \
-F "caption=Test park upload"
# Test with non-existent entity (should return 400 error)
curl -X POST "http://localhost:8000/api/v1/rides/99999/photos/" \
-H "Authorization: Bearer your_test_token" \
-F "image=@test_image.jpg" \
-F "caption=Test upload"
2. Verify Image Variants
# Django shell verification
from apps.rides.models import RidePhoto
photo = RidePhoto.objects.first()
print(f"Image URL: {photo.image.url}")
print(f"Thumbnail: {photo.image.url.replace('/public', '/thumbnail')}")
print(f"Medium: {photo.image.url.replace('/public', '/medium')}")
print(f"Large: {photo.image.url.replace('/public', '/large')}")
3. Test Transformations
Visit these URLs in your browser to verify transformations work:
- Original:
https://imagedelivery.net/your-hash/image-id/public - Resized:
https://imagedelivery.net/your-hash/image-id/public/w=400 - WebP:
https://imagedelivery.net/your-hash/image-id/public/format=webp
Future Enhancements
Potential future improvements:
- Signed URLs for private images
- Batch upload capabilities
- Image analytics integration
- Advanced AI-powered transformations
- Custom watermarking
- Automatic alt-text generation
Dependencies
django-cloudflare-images>=0.6.0(already installed)- Proper environment variables configured
- Cloudflare Images account setup