Usage
This guide covers the various ways to use django-cloudflareimages-toolkit in your Django applications.
Model Field Usage
The simplest way to use Cloudflare Images is with the CloudflareImageField:
from django.db import models
from django_cloudflareimages_toolkit.fields import CloudflareImageField
class Profile(models.Model):
name = models.CharField(max_length=100)
avatar = CloudflareImageField()
class Product(models.Model):
name = models.CharField(max_length=100)
description = models.TextField()
image = CloudflareImageField()
Tracking Image Usage
Declaring a CloudflareImageField automatically opts the model into the image
usage registry. Each such field is auto-discovered, and saving or deleting the
model keeps an ImageUsage row in sync, so you can always answer “which content
references this image?”:
product = Product.objects.create(image="cloudflare-image-id")
product.image_obj = product.image # value is just the Cloudflare ID
image = CloudflareImage.objects.get(cloudflare_id="cloudflare-image-id")
image.usages.all() # -> the product reference
# Reverse lookups for operations/auditing:
CloudflareImage.objects.filter(usages__isnull=True) # orphaned (unused) images
ImageUsage.objects.filter(image__isnull=True) # referenced but unregistered
For references stored outside a CloudflareImageField (e.g. an ID kept in a
JSONField or derived at runtime), record them explicitly:
from django_cloudflareimages_toolkit import register_usage, unregister_usage
register_usage(obj, "cloudflare-image-id") # field_name="manual" by default
unregister_usage(obj)
Bulk operations (QuerySet.update(), bulk_create, loaddata) bypass the
sync signals; run python manage.py reconcile_image_usage to rebuild the
registry (it is idempotent). The admin exposes a thumbnail gallery, a “Used by”
panel, and Orphaned/Unregistered filters built on this data.
Direct Upload Service
For programmatic image uploads, use CloudflareImagesService. The
service exposes two related operations: generating a one-time upload
URL (for handing to a browser or another service) and the full
end-to-end flow of uploading a local file from disk straight to
Cloudflare.
Generating a direct upload URL
from django_cloudflareimages_toolkit.services import cloudflare_service
# Returns a CloudflareImage row with the upload_url populated and
# status=PENDING. The same row will move to UPLOADED once Cloudflare
# finishes processing the upload (you can poll via check_image_status
# or wire up the WebhookView — see docs/webhooks.rst).
image = cloudflare_service.create_direct_upload_url(
user=request.user,
metadata={"category": "profile", "user_id": str(request.user.pk)},
require_signed_urls=False,
expiry_minutes=30,
)
print(image.cloudflare_id) # e.g. "2cdc28f0-017a-49c4-9ed7-..."
print(image.upload_url) # one-time URL — POST the file here
The full signature is:
create_direct_upload_url(
user=None,
custom_id=None,
metadata=None,
require_signed_urls=None,
expiry_minutes=None,
creator=None,
)
Any argument left as None falls back to its settings default
(REQUIRE_SIGNED_URLS, DEFAULT_EXPIRY_MINUTES, DEFAULT_METADATA,
DEFAULT_CREATOR). The resolved metadata and creator are sent to
Cloudflare’s /images/v2/direct_upload call and round-tripped onto the
local CloudflareImage row.
Tagging uploads with a creator
Pass creator to associate an upload with a Cloudflare “creator” value.
It is persisted on the local row and is queryable:
image = cloudflare_service.create_direct_upload_url(
user=request.user,
creator="user-123",
)
# The creator is stored on the model and indexed for filtering.
CloudflareImage.objects.filter(creator="user-123")
When creator is omitted, the DEFAULT_CREATOR setting is used. Pass an
explicit empty string (creator="") — also accepted by the REST endpoint — to
force an untagged upload that bypasses DEFAULT_CREATOR.
Customizing metadata with a factory
For server-side control over the metadata attached to every upload,
configure a metadata factory. Subclass ImageMetadataFactory and
override get_metadata:
from django_cloudflareimages_toolkit import ImageMetadataFactory
class TenantMetadataFactory(ImageMetadataFactory):
def get_metadata(self, *, metadata, user=None, **context):
if user is not None:
metadata['uploaded_by'] = str(user.pk)
metadata['source'] = 'web'
return metadata
The signature you override is:
def get_metadata(self, *, metadata, user=None, custom_id=None,
creator=None, **context) -> dict
Factory instances are callable. Register it via the METADATA_FACTORY
setting (a dotted import path string, a class, an instance, or any callable):
CLOUDFLARE_IMAGES = {
# ... other settings
'METADATA_FACTORY': 'myapp.factories.TenantMetadataFactory',
}
The factory receives the already-resolved metadata (DEFAULT_METADATA
merged with per-request metadata) plus upload context, and returns the
final metadata dict that is sent to Cloudflare and persisted. The merge
precedence, lowest to highest, is:
DEFAULT_METADATA < per-request metadata < factory output
The factory is trusted server-side code and has the final say — it can augment or override keys supplied by the client.
Server-side: uploading a local file from disk
A common pattern is to upload an image stored on the Django server
(generated thumbnail, imported asset, content-management workflow)
straight to Cloudflare without involving a browser. The toolkit drives
this through the same Direct Creator Upload primitive — get an upload
URL from Cloudflare, then POST the file bytes to it.
import requests
from django_cloudflareimages_toolkit.services import cloudflare_service
from django_cloudflareimages_toolkit.exceptions import CloudflareImagesError
def upload_local_image(path: str, *, user=None, metadata=None):
"""Upload a file from disk to Cloudflare Images.
Returns the populated CloudflareImage row.
"""
# 1. Reserve a one-time upload slot. The returned CloudflareImage
# is already persisted with status=PENDING, so a webhook or
# a later check_image_status() call has a row to update.
image = cloudflare_service.create_direct_upload_url(
user=user,
metadata=metadata or {},
expiry_minutes=30,
)
# 2. POST the bytes. Cloudflare's Direct Creator endpoint expects
# multipart/form-data with the file under the field name "file".
with open(path, "rb") as fh:
response = requests.post(
image.upload_url,
files={"file": (image.cloudflare_id, fh, "application/octet-stream")},
timeout=60,
)
if not response.ok:
raise CloudflareImagesError(
f"Cloudflare upload POST failed: {response.status_code} {response.text[:200]}"
)
# 3. Sync the local row with the now-uploaded state. This is
# idempotent — the webhook (if configured) will also drive
# this transition.
cloudflare_service.check_image_status(image)
image.refresh_from_db()
return image
# Usage
image = upload_local_image(
"/srv/media/incoming/banner.jpg",
user=request.user,
metadata={"source": "cms_import"},
)
print(image.status) # "uploaded"
print(image.public_url) # delivery URL
print(image.get_variant_url("thumbnail"))
The same pattern works for BytesIO and InMemoryUploadedFile —
swap the open(path, "rb") line for the file-like object you already
have.
Note
The upload URL is single-use and expires per expiry_minutes (2–360
minutes; default 30). If you batch hundreds of uploads, request one
URL per file rather than reusing — Cloudflare rejects reuse.
For production-grade retry, circuit-breaking, and graceful degradation around this flow, see Patterns & Recipes.
Bulk uploads
For batch jobs, push each upload_local_image call through a task
queue rather than running them inline. A Celery example:
from celery import shared_task
from django_cloudflareimages_toolkit.exceptions import CloudflareImagesError
@shared_task(
bind=True,
autoretry_for=(CloudflareImagesError, requests.RequestException),
retry_backoff=True,
retry_backoff_max=300,
retry_jitter=True,
max_retries=8,
)
def upload_local_image_async(self, path: str, *, user_id: int | None = None):
from django.contrib.auth import get_user_model
user = get_user_model().objects.get(pk=user_id) if user_id else None
return upload_local_image(path, user=user).cloudflare_id
This pattern is covered in more detail (with circuit-breaker + cache fallback) in Patterns & Recipes.
Frontend Integration
Use the direct upload URLs for secure client-side uploads:
JavaScript Upload Example
<!-- HTML form -->
<form id="upload-form">
<input type="file" id="image-input" accept="image/*">
<button type="submit">Upload Image</button>
<div id="progress"></div>
</form>
// JavaScript upload handler
document.getElementById('upload-form').addEventListener('submit', async (e) => {
e.preventDefault();
const fileInput = document.getElementById('image-input');
const file = fileInput.files[0];
if (!file) return;
try {
// Get upload URL from your Django backend
const response = await fetch('/api/get-upload-url/');
const uploadData = await response.json();
// Upload directly to Cloudflare
const formData = new FormData();
formData.append('file', file);
const uploadResponse = await fetch(uploadData.uploadURL, {
method: 'POST',
body: formData
});
if (uploadResponse.ok) {
const result = await uploadResponse.json();
console.log('Upload successful:', result);
// Save image reference in your Django app
await fetch('/api/save-image/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCookie('csrftoken')
},
body: JSON.stringify({
cloudflare_id: result.result.id,
filename: file.name
})
});
}
} catch (error) {
console.error('Upload failed:', error);
}
});
Django Views for Upload
Create views to handle upload URL generation and image saving:
Upload URL Generation
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
from django.contrib.auth.decorators import login_required
from django_cloudflareimages_toolkit.services import CloudflareImagesService
@login_required
def get_upload_url(request):
try:
service = CloudflareImagesService()
upload_data = service.get_direct_upload_url()
return JsonResponse({
'uploadURL': upload_data['uploadURL'],
'id': upload_data['id']
})
except Exception as e:
return JsonResponse({'error': str(e)}, status=500)
Image Saving
When a client reports back the cloudflare_id it uploaded, use
CloudflareImage.objects.register_uploaded to register it safely. The
manager fetches the image from Cloudflare, confirms it exists and that its
draft state is cleared, then creates (or returns) the local record
populated with status, variants, metadata, and creator.
import json
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
from django.contrib.auth.decorators import login_required
from django_cloudflareimages_toolkit.models import CloudflareImage
from django_cloudflareimages_toolkit.exceptions import (
ImageNotFoundError, ImageNotReadyError,
)
@csrf_exempt
@login_required
def save_image(request):
if request.method == 'POST':
data = json.loads(request.body)
try:
image = CloudflareImage.objects.register_uploaded(
data['cloudflare_id'],
user=request.user,
)
except ImageNotFoundError:
return JsonResponse({'error': 'Image not found in Cloudflare'}, status=404)
except ImageNotReadyError:
return JsonResponse({'error': 'Upload not completed yet'}, status=409)
return JsonResponse({
'success': True,
'image_id': image.id,
'url': image.get_url()
})
return JsonResponse({'error': 'Method not allowed'}, status=405)
Warning
Do not call CloudflareImage.objects.get_or_create(cloudflare_id=...)
directly with a client-supplied id. The id may not exist, may still be a
draft, or may belong to another user, and doing so leaves a bare local row
that does not correspond to a real Cloudflare image.
register_uploaded is the recommended path: it validates the id against
Cloudflare before persisting anything, and raises ImageNotFoundError
(id does not exist) or ImageNotReadyError (exists but still a draft)
without creating a local row on failure. When you set creator at upload
time, also pass expected_creator=str(request.user.pk) so a caller can
only register their own image — a mismatch raises ImageOwnershipError.
Using in Django Forms
Integrate Cloudflare Images with Django forms:
Form Definition
from django import forms
from django_cloudflareimages_toolkit.fields import CloudflareImageField
class ProfileForm(forms.ModelForm):
class Meta:
model = Profile
fields = ['name', 'avatar']
widgets = {
'avatar': forms.HiddenInput(), # Hidden field for image ID
}
class ProductForm(forms.Form):
name = forms.CharField(max_length=100)
description = forms.CharField(widget=forms.Textarea)
image = forms.CharField(widget=forms.HiddenInput()) # Store Cloudflare ID
Form Template
<!-- templates/profile_form.html -->
<form method="post" id="profile-form">
{% csrf_token %}
{{ form.name }}
<!-- Custom image upload widget -->
<div class="image-upload">
<input type="file" id="image-input" accept="image/*">
<div id="image-preview"></div>
{{ form.avatar }} <!-- Hidden field -->
</div>
<button type="submit">Save Profile</button>
</form>
<script>
// Handle image upload and form submission
document.getElementById('image-input').addEventListener('change', async (e) => {
const file = e.target.files[0];
if (!file) return;
// Upload to Cloudflare and update hidden field
const uploadData = await uploadToCloudflare(file);
document.getElementById('id_avatar').value = uploadData.id;
// Show preview
const preview = document.getElementById('image-preview');
preview.innerHTML = `<img src="${uploadData.url}" style="max-width: 200px;">`;
});
</script>
Image Transformations
The toolkit provides powerful image transformation capabilities using Cloudflare’s flexible variants and Image Resizing features.
CloudflareImageTransform
Use CloudflareImageTransform to build transformation URLs:
from django_cloudflareimages_toolkit.transformations import CloudflareImageTransform
# For Cloudflare Images (imagedelivery.net)
# Transforms are applied as path-based options: width=300,height=200
transform = CloudflareImageTransform(image.public_url)
thumbnail_url = (transform
.width(300)
.height(300)
.fit('cover')
.quality(85)
.build())
# Result: https://imagedelivery.net/<hash>/<id>/width=300,height=300,fit=cover,quality=85
# For Image Resizing on custom domains (cdn-cgi format)
transform = CloudflareImageTransform("/images/photo.jpg", zone="example.com")
resized_url = transform.width(800).quality(85).build()
# Result: https://example.com/cdn-cgi/image/width=800,quality=85/images/photo.jpg
Available Transformations
transform = CloudflareImageTransform(base_url)
# Dimensions
transform.width(800) # Width in pixels (1-12000)
transform.height(600) # Height in pixels (1-12000)
# Fit modes
transform.fit('scale-down') # Scale down only, never enlarge
transform.fit('contain') # Fit within dimensions, preserve aspect ratio
transform.fit('cover') # Fill dimensions, crop if needed
transform.fit('crop') # Crop to exact dimensions
transform.fit('pad') # Pad to dimensions with background
# Quality and format
transform.quality(85) # Quality 1-100
transform.format('webp') # Output format: auto, webp, avif, jpeg, json
# Visual adjustments
transform.blur(10) # Blur amount 1-250
transform.sharpen(2.0) # Sharpen 0.0-10.0
transform.brightness(0.1) # Brightness -1.0 to 1.0
transform.contrast(0.1) # Contrast -1.0 to 1.0
transform.gamma(1.2) # Gamma 0.1-9.9
# Cropping and positioning
transform.gravity('auto') # auto, left, right, top, bottom, center
transform.rotate(90) # Rotation: 0, 90, 180, 270
# Borders and background
transform.background('ffffff') # Background color (hex)
transform.border(2, 'cccccc') # Border width and color
# Device pixel ratio
transform.dpr(2.0) # DPR 1.0-3.0 for retina displays
Predefined Variants
Use CloudflareImageVariants for common use cases:
from django_cloudflareimages_toolkit.transformations import CloudflareImageVariants
# Square thumbnail
thumbnail = CloudflareImageVariants.thumbnail(image.public_url, 150)
# Circular avatar (requires CSS border-radius)
avatar = CloudflareImageVariants.avatar(image.public_url, 100)
# Hero/banner image
hero = CloudflareImageVariants.hero_image(image.public_url, 1920, 800)
# Responsive image
responsive = CloudflareImageVariants.responsive_image(image.public_url, 800)
# Product image with white background
product = CloudflareImageVariants.product_image(image.public_url, 400)
# Mobile-optimized WebP
mobile = CloudflareImageVariants.mobile_optimized(image.public_url, 400)
Responsive Images
Generate srcset for responsive images:
from django_cloudflareimages_toolkit.transformations import CloudflareImageUtils
# Generate srcset attribute
srcset = CloudflareImageUtils.get_srcset(
image.public_url,
widths=[320, 640, 1024, 1920],
quality=85
)
# Result: "url 320w, url 640w, url 1024w, url 1920w"
# Generate sizes attribute
sizes = CloudflareImageUtils.get_sizes_attribute({
'max-width: 768px': 100, # 100vw on mobile
'max-width: 1024px': 50, # 50vw on tablet
'default': 800 # 800px on desktop
})
Template Usage
Display images in your Django templates:
Basic Image Display
{% load cloudflare_images %}
<!-- Display original image (get_url renders the public variant) -->
<img src="{{ profile.avatar.get_url }}" alt="Profile Avatar">
<!-- Derive a thumbnail from the public URL with a template tag -->
<img src="{% cf_thumbnail profile.avatar.get_url 200 %}" alt="Avatar Thumbnail">
<!-- Display with fallback -->
{% if profile.avatar %}
<img src="{{ profile.avatar.get_url }}" alt="Profile Avatar">
{% else %}
<img src="{% static 'images/default-avatar.png' %}" alt="Default Avatar">
{% endif %}
Advanced Template Usage
{% load cloudflare_images %}
<!-- Product gallery -->
<div class="product-gallery">
{% for product in products %}
<div class="product-card">
<img src="{% cf_thumbnail product.image.get_url 300 %}"
alt="{{ product.name }}"
onclick="showLargeImage('{{ product.image.get_url }}')">
<h3>{{ product.name }}</h3>
<p>{{ product.description|truncatewords:20 }}</p>
</div>
{% endfor %}
</div>
Image Management
Programmatically manage images using the service:
List Images
from django_cloudflareimages_toolkit.services import CloudflareImagesService
service = CloudflareImagesService()
# List all images
images = service.list_images()
for image in images['result']['images']:
print(f"Image ID: {image['id']}")
print(f"Filename: {image['filename']}")
print(f"Uploaded: {image['uploaded']}")
Get Image Details
# Get specific image details
image_id = "your-image-id"
image_details = service.get_image(image_id)
print(f"Image URL: {image_details['result']['variants'][0]}")
print(f"Metadata: {image_details['result']['meta']}")
Delete Images
from django_cloudflareimages_toolkit.models import CloudflareImage
# Delete an image: pass the CloudflareImage instance (delete_image returns a bool)
image = CloudflareImage.objects.get(cloudflare_id="your-image-id")
if service.delete_image(image):
image.delete() # also remove the local tracking row
print("Image deleted successfully")
Webhook Handling
Handle real-time upload notifications. The package already ships a webhook
endpoint — WebhookView at
<prefix>/api/webhook/ — that validates the signature, updates the matching
CloudflareImage, and returns the documented status codes, so most projects
just mount the URLs and react via a post_save signal on CloudflareImage.
The view below is only illustrative if you need a fully custom endpoint; note
that Cloudflare delivers the image object itself (id, uploaded,
draft, variants …), not an {event, data} envelope.
Webhook View
import json
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt
from django_cloudflareimages_toolkit.services import cloudflare_service
@csrf_exempt
def cloudflare_webhook(request):
if request.method != 'POST':
return HttpResponse(status=405)
# Authenticate the call yourself only if you are NOT using the bundled
# WebhookView (which already validates the signature when WEBHOOK_SECRET
# is set). validate_webhook_signature returns True when no secret is
# configured, so this is a no-op in development.
signature = request.headers.get('X-Signature') or request.headers.get(
'X-Cloudflare-Signature'
)
if not cloudflare_service.validate_webhook_signature(
request.body, signature or ''
):
return HttpResponse(status=401)
try:
payload = json.loads(request.body)
except json.JSONDecodeError:
return HttpResponse(status=400)
# Cloudflare delivers the image object at the TOP LEVEL
# ({"id": ..., "uploaded": ..., "draft": ..., "variants": [...]}),
# not an {event, data} envelope. process_webhook looks it up by
# payload['id'], updates the local row from those fields, and returns
# the CloudflareImage (or None if the id is unknown) -- the same code
# path the bundled WebhookView uses.
image = cloudflare_service.process_webhook(payload)
if image is None:
return HttpResponse(status=404)
return HttpResponse(status=200)
Admin Integration
The package provides Django admin integration:
Custom Admin Configuration
from django.contrib import admin
from django_cloudflareimages_toolkit.admin import CloudflareImageAdmin
from django_cloudflareimages_toolkit.models import CloudflareImage
# Customize the admin interface
@admin.register(CloudflareImage)
class CustomCloudflareImageAdmin(CloudflareImageAdmin):
list_display = ['filename', 'uploaded_at', 'file_size', 'is_ready', 'image_preview']
list_filter = ['is_ready', 'format', 'uploaded_at']
search_fields = ['filename', 'cloudflare_id']
readonly_fields = ['cloudflare_id', 'uploaded_at', 'file_size', 'width', 'height']
def image_preview(self, obj):
if obj.is_ready:
return f'<img src="{obj.get_url("thumbnail")}" style="max-height: 50px;">'
return "Processing..."
image_preview.allow_tags = True
image_preview.short_description = "Preview"
Management Commands
Use the provided management commands:
Cleanup Expired Images
# Clean up expired upload URLs
python manage.py cleanup_expired_images
# Clean up images older than 7 days
python manage.py cleanup_expired_images --days 7
# Dry run to see what would be deleted
python manage.py cleanup_expired_images --dry-run
Testing
Test image functionality in your Django tests:
from django.test import TestCase
from unittest.mock import patch, MagicMock
from django_cloudflareimages_toolkit.services import CloudflareImagesService
from django_cloudflareimages_toolkit.models import CloudflareImage
class CloudflareImagesTestCase(TestCase):
@patch('django_cloudflareimages_toolkit.services.requests.post')
def test_get_direct_upload_url(self, mock_post):
# Mock the API response
mock_response = MagicMock()
mock_response.json.return_value = {
'success': True,
'result': {
'id': 'test-image-id',
'uploadURL': 'https://upload.imagedelivery.net/test-url'
}
}
mock_post.return_value = mock_response
service = CloudflareImagesService()
result = service.get_direct_upload_url()
self.assertEqual(result['id'], 'test-image-id')
self.assertIn('uploadURL', result)
def test_cloudflare_image_model(self):
from django.utils import timezone
image = CloudflareImage.objects.create(
cloudflare_id='test-id',
filename='test.jpg',
expires_at=timezone.now() + timezone.timedelta(minutes=30),
)
# __str__ is "CloudflareImage(<cloudflare_id>) - <status>"; a fresh
# row defaults to status "pending" and is not yet ready.
self.assertEqual(str(image), 'CloudflareImage(test-id) - pending')
self.assertFalse(image.is_ready)
Best Practices
Security First: Always verify webhook signatures and validate uploads
Error Handling: Implement proper error handling for upload failures
User Feedback: Provide clear feedback during upload processes
Image Optimization: Use appropriate variants for different use cases
Cleanup: Regularly clean up expired upload URLs and unused images
Testing: Test upload functionality across different browsers and devices
Monitoring: Monitor upload success rates and performance
Backup Strategy: Consider backup strategies for critical images
Rate Limiting: Implement rate limiting for upload endpoints
Progressive Enhancement: Ensure your app works without JavaScript for uploads
Performance Tips
Use Variants: Create and use appropriate image variants instead of resizing originals
Lazy Loading: Implement lazy loading for image-heavy pages
CDN Benefits: Leverage Cloudflare’s global CDN for fast image delivery
Async Uploads: Use asynchronous uploads to improve user experience
Batch Operations: Batch multiple image operations when possible
Caching: Cache image URLs and metadata appropriately
Compression: Use appropriate image formats and compression settings