Source code for django_cloudflareimages_toolkit.widgets

"""
Django form widgets for Cloudflare Images integration.

This module provides custom form widgets for handling image uploads
with Cloudflare Images, including JavaScript-based upload functionality.
"""

import json
from typing import Any

from django import forms
from django.forms.renderers import get_default_renderer
from django.utils.safestring import SafeText, mark_safe


[docs] class CloudflareImageWidget(forms.TextInput): """ A widget for handling Cloudflare image uploads. This widget provides a file input interface that handles direct uploads to Cloudflare Images and stores the resulting image ID in the form field. """ template_name = ( "django_cloudflareimages_toolkit/widgets/cloudflare_image_widget.html" )
[docs] def __init__( self, variants: list[str] | None = None, metadata: dict[str, Any] | None = None, require_signed_urls: bool = False, max_file_size: int | None = None, allowed_formats: list[str] | None = None, attrs: dict[str, Any] | None = None, ): """ Initialize the widget. Args: variants: List of image variants to create metadata: Default metadata for uploads require_signed_urls: Whether to require signed URLs max_file_size: Maximum file size in bytes allowed_formats: List of allowed image formats attrs: Additional HTML attributes """ self.variants = variants or [] self.metadata = metadata or {} self.require_signed_urls = require_signed_urls self.max_file_size = max_file_size self.allowed_formats = allowed_formats or ["jpeg", "png", "gif", "webp"] default_attrs = {"type": "hidden", "class": "cloudflare-image-field"} if attrs: default_attrs.update(attrs) super().__init__(attrs=default_attrs)
[docs] def format_value(self, value): """Format the field value for display.""" if value is None: return "" return str(value)
[docs] def render( self, name: str, value: Any, attrs: dict[str, Any] | None = None, renderer=None ) -> SafeText: """ Render the widget HTML. Args: name: Field name value: Current field value attrs: HTML attributes renderer: Template renderer Returns: Rendered HTML string """ if renderer is None: renderer = get_default_renderer() context = self.get_context(name, value, attrs) context["widget"].update( { "variants": self.variants, "metadata": self.metadata, "require_signed_urls": self.require_signed_urls, "max_file_size": self.max_file_size, "allowed_formats": self.allowed_formats, "config_json": mark_safe( json.dumps( { "variants": self.variants, "metadata": self.metadata, "require_signed_urls": self.require_signed_urls, "max_file_size": self.max_file_size, "allowed_formats": self.allowed_formats, } ) ), } ) # Fallback HTML if template is not found try: return renderer.render(self.template_name, context) except Exception: return self._render_fallback(name, value, attrs)
def _render_fallback( self, name: str, value: Any, attrs: dict[str, Any] | None = None ) -> SafeText: """ Render fallback HTML when template is not available. Args: name: Field name value: Current field value attrs: HTML attributes Returns: Fallback HTML string """ if attrs is None: attrs = {} # Merge widget attrs final_attrs = self.build_attrs(attrs) # Create unique IDs for elements field_id = final_attrs.get("id", f"id_{name}") upload_id = f"{field_id}_upload" preview_id = f"{field_id}_preview" progress_id = f"{field_id}_progress" # Build the HTML html_parts = [ # Hidden input for storing the image ID f'<input type="hidden" name="{name}" id="{field_id}" value="{self.format_value(value)}" />', # File input for selecting images '<div class="cloudflare-image-upload-container">', f' <input type="file" id="{upload_id}" accept="image/*" class="cloudflare-image-upload" />', f' <div id="{preview_id}" class="cloudflare-image-preview"></div>', f' <div id="{progress_id}" class="cloudflare-image-progress" style="display: none;">', ' <div class="progress-bar"></div>', ' <span class="progress-text">Uploading...</span>', " </div>", "</div>", # JavaScript for handling uploads "<script>", "(function() {", f" const config = {json.dumps({'variants': self.variants, 'metadata': self.metadata, 'require_signed_urls': self.require_signed_urls, 'max_file_size': self.max_file_size, 'allowed_formats': self.allowed_formats})};", f' const fieldId = "{field_id}";', f' const uploadId = "{upload_id}";', f' const previewId = "{preview_id}";', f' const progressId = "{progress_id}";', " ", " // Initialize the upload handler when DOM is ready", ' if (document.readyState === "loading") {', ' document.addEventListener("DOMContentLoaded", initUploadHandler);', " } else {", " initUploadHandler();", " }", " ", " function initUploadHandler() {", " const uploadInput = document.getElementById(uploadId);", " const hiddenInput = document.getElementById(fieldId);", " const previewDiv = document.getElementById(previewId);", " const progressDiv = document.getElementById(progressId);", " ", " if (!uploadInput) return;", " ", ' uploadInput.addEventListener("change", handleFileSelect);', " ", " // Show current image if value exists", " if (hiddenInput.value) {", " showImagePreview(hiddenInput.value);", " }", " }", " ", " function handleFileSelect(event) {", " const file = event.target.files[0];", " if (!file) return;", " ", " // Validate file", " if (!validateFile(file)) return;", " ", " // Start upload", " uploadFile(file);", " }", " ", " function validateFile(file) {", " const maxSize = config.max_file_size;", " const allowedFormats = config.allowed_formats;", " ", " if (maxSize && file.size > maxSize) {", ' alert("File size exceeds maximum allowed size");', " return false;", " }", " ", ' const fileType = file.type.split("/")[1];', " if (allowedFormats.length && !allowedFormats.includes(fileType)) {", ' alert("File format not allowed");', " return false;", " }", " ", " return true;", " }", " ", " async function uploadFile(file) {", " const progressDiv = document.getElementById(progressId);", " const previewDiv = document.getElementById(previewId);", " const hiddenInput = document.getElementById(fieldId);", " ", " try {", " // Show progress", ' progressDiv.style.display = "block";', ' previewDiv.innerHTML = "";', " ", " // Get upload URL from Django backend", ' const uploadUrlResponse = await fetch("/cloudflare-images/get-upload-url/", {', ' method: "POST",', " headers: {", ' "Content-Type": "application/json",', ' "X-CSRFToken": getCsrfToken()', " },", " body: JSON.stringify({", " metadata: config.metadata,", " require_signed_urls: config.require_signed_urls", " })", " });", " ", " if (!uploadUrlResponse.ok) {", ' throw new Error("Failed to get upload URL");', " }", " ", " const uploadData = await uploadUrlResponse.json();", " ", " // Upload file to Cloudflare", " const formData = new FormData();", ' formData.append("file", file);', " ", " const uploadResponse = await fetch(uploadData.uploadURL, {", ' method: "POST",', " body: formData", " });", " ", " if (!uploadResponse.ok) {", ' throw new Error("Upload failed");', " }", " ", " const result = await uploadResponse.json();", " ", " // Update hidden input with image ID", " hiddenInput.value = result.result.id;", " ", " // Show preview", " showImagePreview(result.result.id);", " ", " // Hide progress", ' progressDiv.style.display = "none";', " ", " } catch (error) {", ' console.error("Upload error:", error);', ' alert("Upload failed: " + error.message);', ' progressDiv.style.display = "none";', " }", " }", " ", " function showImagePreview(imageId) {", " const previewDiv = document.getElementById(previewId);", " if (!imageId) return;", " ", " // Create preview image (you may need to adjust the URL format)", ' const img = document.createElement("img");', ' img.src = "/cloudflare-images/image/" + imageId + "/thumbnail/";', ' img.style.maxWidth = "200px";', ' img.style.maxHeight = "200px";', ' img.alt = "Image preview";', " ", ' previewDiv.innerHTML = "";', " previewDiv.appendChild(img);", " }", " ", " function getCsrfToken() {", ' const cookies = document.cookie.split(";");', " for (let cookie of cookies) {", ' const [name, value] = cookie.trim().split("=");', ' if (name === "csrftoken") {', " return value;", " }", " }", ' return "";', " }", "})();", "</script>", # Basic CSS for styling "<style>", ".cloudflare-image-upload-container {", " border: 2px dashed #ccc;", " border-radius: 4px;", " padding: 20px;", " text-align: center;", " margin: 10px 0;", "}", ".cloudflare-image-preview img {", " border-radius: 4px;", " box-shadow: 0 2px 4px rgba(0,0,0,0.1);", "}", ".cloudflare-image-progress {", " margin-top: 10px;", "}", ".progress-bar {", " width: 100%;", " height: 4px;", " background: #f0f0f0;", " border-radius: 2px;", " overflow: hidden;", "}", ".progress-bar::after {", ' content: "";', " display: block;", " width: 100%;", " height: 100%;", " background: #007cba;", " animation: progress 2s infinite;", "}", "@keyframes progress {", " 0% { transform: translateX(-100%); }", " 100% { transform: translateX(100%); }", "}", "</style>", ] return mark_safe("".join(html_parts))
[docs] class Media: """Define media files for the widget.""" css = { "all": ("django_cloudflareimages_toolkit/css/cloudflare_image_widget.css",) } js = ("django_cloudflareimages_toolkit/js/cloudflare_image_widget.js",)