Files
thrillwiki_django_no_react/docs/cloudflare_images_integration.md
2025-09-21 20:19:12 -04:00

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.ImageField to CloudflareImagesField(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.ImageField to CloudflareImagesField(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_url field: Full URL to the Cloudflare Images asset
    • Added image_variants field: Dictionary of available image variants with URLs
    • Proper DRF Spectacular schema decorations with examples
    • Maintains backward compatibility

Parks API (backend/apps/api/v1/parks/serializers.py)

  • ParkPhotoOutputSerializer: Enhanced with Cloudflare Images support
    • Added image_url field: Full URL to the Cloudflare Images asset
    • Added image_variants field: Dictionary of available image variants with URLs
    • Proper DRF Spectacular schema decorations with examples
    • Maintains backward compatibility

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

  1. Performance: Cloudflare's global CDN ensures fast image delivery
  2. Optimization: Automatic image optimization and format conversion
  3. Variants: Multiple image sizes generated automatically
  4. Scalability: No local storage requirements
  5. API Documentation: Complete OpenAPI schema with examples
  6. Backward Compatibility: Existing API consumers continue to work
  7. Entity Validation: Photos are always associated with valid rides or parks
  8. Data Integrity: Prevents orphaned photos without parent entities
  9. Automatic Photo Inclusion: Photos are automatically included when displaying rides and parks
  10. 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:

  1. Go to Cloudflare Images dashboard
  2. Navigate to "Variants" section
  3. Create new variant with desired transformations
  4. 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=auto to serve optimal format (WebP, AVIF) based on browser support
  • Set appropriate quality levels (80-85 for photos, 90+ for graphics)
  • Use fit=cover for 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