# 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`: ```python # 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: ```json { "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) ```json { "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) ```json { "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:** ```bash Authorization: Bearer Content-Type: multipart/form-data ``` **cURL Example:** ```bash 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):** ```json { "detail": "Ride not found" } ``` **Python Example:** ```python 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:** ```javascript 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:** ```bash 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):** ```json { "detail": "Park not found" } ``` ### 3. Upload Response Format Both endpoints return the same enhanced format with Cloudflare Images integration: ```json { "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: ```python # In your model class RidePhoto(TrackedModel): image = CloudflareImagesField(variant="hero_banner") # Custom variant ``` ### 4. Responsive Images Implementation Use different variants for responsive design: ```html Ride photo ``` ```css /* 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 ```bash # 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 ```python # 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