"""
Django admin configuration for Cloudflare Images Direct Creator Upload.
This module provides comprehensive admin interfaces for monitoring and managing
Cloudflare images, upload logs, and system statistics.
"""
import json
from django.contrib import admin
from django.urls import reverse
from django.utils import timezone
from django.utils.html import format_html, format_html_join
from .exceptions import CloudflareImagesError
from .models import CloudflareImage, ImageUploadLog, ImageUploadStatus
from .services import cloudflare_service
class ImageUploadLogInline(admin.TabularInline):
"""Inline admin for image upload logs."""
model = ImageUploadLog
extra = 0
readonly_fields = ("timestamp", "event_type", "message", "formatted_data")
fields = ("timestamp", "event_type", "message", "formatted_data")
ordering = ("-timestamp",)
def formatted_data(self, obj):
"""Format JSON data for display."""
if obj.data:
try:
formatted = json.dumps(obj.data, indent=2)
return format_html('<pre style="font-size: 11px;">{}</pre>', formatted)
except (TypeError, ValueError):
return str(obj.data)
return "-"
formatted_data.short_description = "Data"
[docs]
@admin.register(CloudflareImage)
class CloudflareImageAdmin(admin.ModelAdmin):
"""Admin interface for CloudflareImage model."""
list_display = (
"cloudflare_id_display",
"user_display",
"status_display",
"filename_display",
"file_size_display",
"created_at",
"expires_at",
"is_expired_display",
"thumbnail_preview",
"actions_display",
)
list_filter = (
"status",
"require_signed_urls",
"created_at",
"uploaded_at",
"expires_at",
("user", admin.RelatedOnlyFieldListFilter),
)
search_fields = (
"cloudflare_id",
"filename",
"original_filename",
"user__username",
"user__email",
)
readonly_fields = (
"id",
"cloudflare_id",
"upload_url_display",
"status",
"created_at",
"updated_at",
"uploaded_at",
"expires_at",
"width",
"height",
"format",
"variants_display",
"cloudflare_metadata_display",
"is_expired_display",
"is_uploaded_display",
"is_ready_display",
"public_url_display",
"thumbnail_url_display",
"image_preview",
"transformation_examples",
)
fields = (
"id",
"cloudflare_id",
"user",
"filename",
"original_filename",
"content_type",
"file_size",
"width",
"height",
"format",
"upload_url_display",
"status",
"require_signed_urls",
"metadata",
"created_at",
"updated_at",
"uploaded_at",
"expires_at",
"variants_display",
"cloudflare_metadata_display",
"is_expired_display",
"is_uploaded_display",
"is_ready_display",
"public_url_display",
"thumbnail_url_display",
"image_preview",
"transformation_examples",
)
inlines = [ImageUploadLogInline]
actions = [
"check_status_action",
"mark_as_expired",
"delete_from_cloudflare_action",
"refresh_all_status",
]
list_per_page = 25
date_hierarchy = "created_at"
[docs]
def get_queryset(self, request):
"""Optimize queryset with select_related."""
return (
super()
.get_queryset(request)
.select_related("user")
.prefetch_related("logs")
)
# Display methods
[docs]
def cloudflare_id_display(self, obj):
"""Display Cloudflare ID with copy button."""
if obj.cloudflare_id:
return format_html(
'<span title="Click to copy" style="cursor: pointer; font-family: monospace;" '
"onclick=\"navigator.clipboard.writeText('{}'); "
"this.style.backgroundColor='#90EE90'; "
"setTimeout(() => this.style.backgroundColor='', 1000)\">{}</span>",
obj.cloudflare_id,
obj.cloudflare_id[:20] + "..."
if len(obj.cloudflare_id) > 20
else obj.cloudflare_id,
)
return "-"
cloudflare_id_display.short_description = "Cloudflare ID"
[docs]
def user_display(self, obj):
"""Display user with link to user admin."""
if obj.user:
url = reverse("admin:auth_user_change", args=[obj.user.pk])
return format_html('<a href="{}">{}</a>', url, obj.user.username)
return "-"
user_display.short_description = "User"
[docs]
def status_display(self, obj):
"""Display status with color coding."""
colors = {
ImageUploadStatus.PENDING: "#ffc107", # Yellow
ImageUploadStatus.DRAFT: "#17a2b8", # Blue
ImageUploadStatus.UPLOADED: "#28a745", # Green
ImageUploadStatus.FAILED: "#dc3545", # Red
ImageUploadStatus.EXPIRED: "#6c757d", # Gray
}
color = colors.get(obj.status, "#6c757d")
return format_html(
'<span style="color: {}; font-weight: bold;">{}</span>',
color,
obj.get_status_display(),
)
status_display.short_description = "Status"
[docs]
def filename_display(self, obj):
"""Display filename with truncation."""
if obj.filename:
if len(obj.filename) > 30:
return format_html(
'<span title="{}">{}</span>',
obj.filename,
obj.filename[:27] + "...",
)
return obj.filename
return obj.original_filename or "-"
filename_display.short_description = "Filename"
[docs]
def file_size_display(self, obj):
"""Display file size in human readable format."""
if obj.file_size:
if obj.file_size < 1024:
return f"{obj.file_size} B"
elif obj.file_size < 1024 * 1024:
return f"{obj.file_size / 1024:.1f} KB"
else:
return f"{obj.file_size / (1024 * 1024):.1f} MB"
return "-"
file_size_display.short_description = "File Size"
[docs]
def is_expired_display(self, obj):
"""Display expiry status with icon."""
if obj.is_expired:
return format_html('<span style="color: #dc3545;">{}</span>', "🔴 Expired")
else:
return format_html('<span style="color: #28a745;">{}</span>', "🟢 Valid")
is_expired_display.short_description = "Expiry Status"
[docs]
def thumbnail_preview(self, obj):
"""Display thumbnail preview if available."""
if obj.is_uploaded and obj.thumbnail_url:
return format_html(
'<img src="{}" style="max-width: 50px; max-height: 50px; border-radius: 4px;" />',
obj.thumbnail_url,
)
return "-"
thumbnail_preview.short_description = "Preview"
[docs]
def actions_display(self, obj):
"""Display action buttons."""
actions = []
if obj.status in [ImageUploadStatus.PENDING, ImageUploadStatus.DRAFT]:
actions.append(
format_html(
'<a href="javascript:void(0)" onclick="checkStatus(\'{}\')" '
'style="color: #007cba; text-decoration: none;">🔄 Check</a>',
obj.pk,
)
)
if obj.is_uploaded and obj.public_url:
actions.append(
format_html(
'<a href="{}" target="_blank" style="color: #007cba; text-decoration: none;">👁️ View</a>',
obj.public_url,
)
)
return (
format_html_join(" | ", "{}", ((action,) for action in actions))
if actions
else "-"
)
actions_display.short_description = "Actions"
# Readonly field methods
[docs]
def upload_url_display(self, obj):
"""Display upload URL with security."""
if obj.upload_url and not obj.is_expired:
return format_html(
'<div style="font-family: monospace; font-size: 11px; word-break: break-all; '
'background: #f8f9fa; padding: 8px; border-radius: 4px; max-width: 400px;">'
"<strong>⚠️ Sensitive:</strong> {}</div>",
obj.upload_url,
)
elif obj.is_expired:
return format_html('<span style="color: #dc3545;">{}</span>', "Expired")
return "-"
upload_url_display.short_description = "Upload URL"
[docs]
def variants_display(self, obj):
"""Display available variants."""
if obj.variants:
variants_html = []
for variant in obj.variants:
variants_html.append(
format_html(
'<a href="{}" target="_blank" style="display: block; margin: 2px 0; '
'font-size: 11px; color: #007cba;">{}</a>',
variant,
variant.split("/")[-1] if "/" in variant else variant,
)
)
return format_html("<div>{}</div>", "".join(variants_html))
return "-"
variants_display.short_description = "Variants"
cloudflare_metadata_display.short_description = "Cloudflare Metadata"
[docs]
def is_uploaded_display(self, obj):
"""Display upload status."""
return "✅ Yes" if obj.is_uploaded else "❌ No"
is_uploaded_display.short_description = "Is Uploaded"
[docs]
def is_ready_display(self, obj):
"""Display ready status."""
return "✅ Ready" if obj.is_ready else "❌ Not Ready"
is_ready_display.short_description = "Is Ready"
[docs]
def public_url_display(self, obj):
"""Display public URL with link."""
if obj.public_url:
return format_html(
'<a href="{}" target="_blank" style="font-family: monospace; font-size: 11px;">{}</a>',
obj.public_url,
obj.public_url,
)
return "-"
public_url_display.short_description = "Public URL"
[docs]
def thumbnail_url_display(self, obj):
"""Display thumbnail URL with link."""
if obj.thumbnail_url:
return format_html(
'<a href="{}" target="_blank" style="font-family: monospace; font-size: 11px;">{}</a>',
obj.thumbnail_url,
obj.thumbnail_url,
)
return "-"
thumbnail_url_display.short_description = "Thumbnail URL"
[docs]
def image_preview(self, obj):
"""Display larger image preview."""
if obj.is_uploaded and obj.public_url:
return format_html(
'<div style="text-align: center;">'
'<img src="{}" style="max-width: 300px; max-height: 200px; border: 1px solid #ddd; border-radius: 4px;" />'
"</div>",
obj.thumbnail_url or obj.public_url,
)
return "-"
image_preview.short_description = "Image Preview"
transformation_examples.short_description = "Transformation Examples"
# Admin actions
[docs]
def check_status_action(self, request, queryset):
"""Check status for selected images."""
updated_count = 0
error_count = 0
for image in queryset:
try:
cloudflare_service.check_image_status(image)
updated_count += 1
except CloudflareImagesError:
error_count += 1
if updated_count:
self.message_user(
request, f"Successfully updated status for {updated_count} images."
)
if error_count:
self.message_user(
request, f"Failed to update {error_count} images.", level="WARNING"
)
check_status_action.short_description = "Check status from Cloudflare"
[docs]
def mark_as_expired(self, request, queryset):
"""Mark selected images as expired."""
count = queryset.update(status=ImageUploadStatus.EXPIRED)
self.message_user(request, f"Marked {count} images as expired.")
mark_as_expired.short_description = "Mark as expired"
[docs]
def delete_from_cloudflare_action(self, request, queryset):
"""Delete selected images from Cloudflare."""
deleted_count = 0
error_count = 0
for image in queryset:
try:
cloudflare_service.delete_image(image)
image.delete()
deleted_count += 1
except CloudflareImagesError:
error_count += 1
if deleted_count:
self.message_user(
request, f"Successfully deleted {deleted_count} images from Cloudflare."
)
if error_count:
self.message_user(
request, f"Failed to delete {error_count} images.", level="WARNING"
)
delete_from_cloudflare_action.short_description = "Delete from Cloudflare"
[docs]
def refresh_all_status(self, request, queryset):
"""Refresh status for all non-final status images."""
pending_images = queryset.filter(
status__in=[ImageUploadStatus.PENDING, ImageUploadStatus.DRAFT]
)
updated_count = 0
for image in pending_images:
try:
cloudflare_service.check_image_status(image)
updated_count += 1
except CloudflareImagesError:
pass
self.message_user(request, f"Refreshed status for {updated_count} images.")
refresh_all_status.short_description = "Refresh all pending/draft status"
@admin.register(ImageUploadLog)
class ImageUploadLogAdmin(admin.ModelAdmin):
"""Admin interface for ImageUploadLog model."""
list_display = (
"timestamp",
"image_display",
"event_type",
"message_display",
"user_display",
)
list_filter = (
"event_type",
"timestamp",
("image__user", admin.RelatedOnlyFieldListFilter),
)
search_fields = (
"image__cloudflare_id",
"event_type",
"message",
"image__user__username",
)
readonly_fields = ("image", "event_type", "message", "formatted_data", "timestamp")
fields = ("image", "event_type", "message", "formatted_data", "timestamp")
list_per_page = 50
date_hierarchy = "timestamp"
ordering = ("-timestamp",)
def get_queryset(self, request):
"""Optimize queryset."""
return super().get_queryset(request).select_related("image", "image__user")
def image_display(self, obj):
"""Display image with link."""
if obj.image:
url = reverse(
"admin:django_cloudflareimages_toolkit_cloudflareimage_change",
args=[obj.image.pk],
)
return format_html(
'<a href="{}">{}</a>',
url,
obj.image.cloudflare_id[:20] + "..."
if len(obj.image.cloudflare_id) > 20
else obj.image.cloudflare_id,
)
return "-"
image_display.short_description = "Image"
def message_display(self, obj):
"""Display message with truncation."""
if len(obj.message) > 50:
return format_html(
'<span title="{}">{}</span>', obj.message, obj.message[:47] + "..."
)
return obj.message
message_display.short_description = "Message"
def user_display(self, obj):
"""Display user."""
if obj.image and obj.image.user:
return obj.image.user.username
return "-"
user_display.short_description = "User"
def formatted_data(self, obj):
"""Format JSON data for display."""
if obj.data:
try:
formatted = json.dumps(obj.data, indent=2)
return format_html(
'<pre style="font-size: 11px; max-height: 200px; overflow-y: auto;">{}</pre>',
formatted,
)
except (TypeError, ValueError):
return str(obj.data)
return "-"
formatted_data.short_description = "Data"
# Custom admin site configuration
class CloudflareImagesAdminSite(admin.AdminSite):
"""Custom admin site for Cloudflare Images."""
site_header = "Cloudflare Images Administration"
site_title = "Cloudflare Images Admin"
index_title = "Cloudflare Images Management"
def index(self, request, extra_context=None):
"""Custom index with statistics."""
extra_context = extra_context or {}
# Get statistics
total_images = CloudflareImage.objects.count()
uploaded_images = CloudflareImage.objects.filter(
status=ImageUploadStatus.UPLOADED
).count()
pending_images = CloudflareImage.objects.filter(
status=ImageUploadStatus.PENDING
).count()
expired_images = CloudflareImage.objects.filter(
expires_at__lt=timezone.now()
).count()
# Recent activity
recent_uploads = CloudflareImage.objects.filter(
uploaded_at__isnull=False
).order_by("-uploaded_at")[:5]
recent_logs = ImageUploadLog.objects.select_related("image").order_by(
"-timestamp"
)[:10]
extra_context.update(
{
"cloudflare_stats": {
"total_images": total_images,
"uploaded_images": uploaded_images,
"pending_images": pending_images,
"expired_images": expired_images,
"upload_success_rate": (uploaded_images / total_images * 100)
if total_images > 0
else 0,
},
"recent_uploads": recent_uploads,
"recent_logs": recent_logs,
}
)
return super().index(request, extra_context)
# Register with custom admin site if desired
# cloudflare_admin_site = CloudflareImagesAdminSite(name='cloudflare_admin')
# cloudflare_admin_site.register(CloudflareImage, CloudflareImageAdmin)
# cloudflare_admin_site.register(ImageUploadLog, ImageUploadLogAdmin)