django-cloudflareimages-toolkit 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,50 @@
1
+ """
2
+ Django Cloudflare Images Toolkit
3
+
4
+ A comprehensive Django toolkit that provides secure image upload functionality,
5
+ transformations, and management using Cloudflare Images.
6
+ """
7
+
8
+ __version__ = "1.0.0"
9
+ __author__ = "PacNPal"
10
+
11
+ # Always import transformation utilities (Django-independent)
12
+ from .transformations import (
13
+ CloudflareImageTransform,
14
+ CloudflareImageUtils,
15
+ CloudflareImageVariants,
16
+ )
17
+
18
+ # Try to import Django-dependent components
19
+ try:
20
+ from .models import CloudflareImage, ImageUploadLog, ImageUploadStatus
21
+ from .services import CloudflareImagesError, cloudflare_service
22
+
23
+ _django_available = True
24
+ except (ImportError, Exception):
25
+ # Django not configured or not available
26
+ _django_available = False
27
+ CloudflareImage = None
28
+ ImageUploadLog = None
29
+ ImageUploadStatus = None
30
+ cloudflare_service = None
31
+ CloudflareImagesError = None
32
+
33
+ # Define what gets imported with "from django_cloudflareimages_toolkit import *"
34
+ __all__ = [
35
+ "CloudflareImageTransform",
36
+ "CloudflareImageVariants",
37
+ "CloudflareImageUtils",
38
+ ]
39
+
40
+ # Add Django components if available
41
+ if _django_available:
42
+ __all__.extend(
43
+ [
44
+ "CloudflareImage",
45
+ "ImageUploadLog",
46
+ "ImageUploadStatus",
47
+ "cloudflare_service",
48
+ "CloudflareImagesError",
49
+ ]
50
+ )
@@ -0,0 +1,604 @@
1
+ """
2
+ Django admin configuration for Cloudflare Images Direct Creator Upload.
3
+
4
+ This module provides comprehensive admin interfaces for monitoring and managing
5
+ Cloudflare images, upload logs, and system statistics.
6
+ """
7
+
8
+ import json
9
+
10
+ from django.contrib import admin
11
+ from django.urls import reverse
12
+ from django.utils import timezone
13
+ from django.utils.html import format_html
14
+
15
+ from .models import CloudflareImage, ImageUploadLog, ImageUploadStatus
16
+ from .services import CloudflareImagesError, cloudflare_service
17
+
18
+
19
+ class ImageUploadLogInline(admin.TabularInline):
20
+ """Inline admin for image upload logs."""
21
+
22
+ model = ImageUploadLog
23
+ extra = 0
24
+ readonly_fields = ("timestamp", "event_type", "message", "formatted_data")
25
+ fields = ("timestamp", "event_type", "message", "formatted_data")
26
+ ordering = ("-timestamp",)
27
+
28
+ def formatted_data(self, obj):
29
+ """Format JSON data for display."""
30
+ if obj.data:
31
+ try:
32
+ formatted = json.dumps(obj.data, indent=2)
33
+ return format_html('<pre style="font-size: 11px;">{}</pre>', formatted)
34
+ except (TypeError, ValueError):
35
+ return str(obj.data)
36
+ return "-"
37
+
38
+ formatted_data.short_description = "Data"
39
+
40
+
41
+ @admin.register(CloudflareImage)
42
+ class CloudflareImageAdmin(admin.ModelAdmin):
43
+ """Admin interface for CloudflareImage model."""
44
+
45
+ list_display = (
46
+ "cloudflare_id_display",
47
+ "user_display",
48
+ "status_display",
49
+ "filename_display",
50
+ "file_size_display",
51
+ "created_at",
52
+ "expires_at",
53
+ "is_expired_display",
54
+ "thumbnail_preview",
55
+ "actions_display",
56
+ )
57
+
58
+ list_filter = (
59
+ "status",
60
+ "require_signed_urls",
61
+ "created_at",
62
+ "uploaded_at",
63
+ "expires_at",
64
+ ("user", admin.RelatedOnlyFieldListFilter),
65
+ )
66
+
67
+ search_fields = (
68
+ "cloudflare_id",
69
+ "filename",
70
+ "original_filename",
71
+ "user__username",
72
+ "user__email",
73
+ )
74
+
75
+ readonly_fields = (
76
+ "id",
77
+ "cloudflare_id",
78
+ "upload_url_display",
79
+ "status",
80
+ "created_at",
81
+ "updated_at",
82
+ "uploaded_at",
83
+ "expires_at",
84
+ "variants_display",
85
+ "cloudflare_metadata_display",
86
+ "is_expired_display",
87
+ "is_uploaded_display",
88
+ "public_url_display",
89
+ "thumbnail_url_display",
90
+ "image_preview",
91
+ "transformation_examples",
92
+ )
93
+
94
+ fields = (
95
+ "id",
96
+ "cloudflare_id",
97
+ "user",
98
+ "filename",
99
+ "original_filename",
100
+ "content_type",
101
+ "file_size",
102
+ "upload_url_display",
103
+ "status",
104
+ "require_signed_urls",
105
+ "metadata",
106
+ "created_at",
107
+ "updated_at",
108
+ "uploaded_at",
109
+ "expires_at",
110
+ "variants_display",
111
+ "cloudflare_metadata_display",
112
+ "is_expired_display",
113
+ "is_uploaded_display",
114
+ "public_url_display",
115
+ "thumbnail_url_display",
116
+ "image_preview",
117
+ "transformation_examples",
118
+ )
119
+
120
+ inlines = [ImageUploadLogInline]
121
+
122
+ actions = [
123
+ "check_status_action",
124
+ "mark_as_expired",
125
+ "delete_from_cloudflare_action",
126
+ "refresh_all_status",
127
+ ]
128
+
129
+ list_per_page = 25
130
+ date_hierarchy = "created_at"
131
+
132
+ def get_queryset(self, request):
133
+ """Optimize queryset with select_related."""
134
+ return (
135
+ super()
136
+ .get_queryset(request)
137
+ .select_related("user")
138
+ .prefetch_related("logs")
139
+ )
140
+
141
+ # Display methods
142
+ def cloudflare_id_display(self, obj):
143
+ """Display Cloudflare ID with copy button."""
144
+ if obj.cloudflare_id:
145
+ return format_html(
146
+ '<span title="Click to copy" style="cursor: pointer; font-family: monospace;" '
147
+ "onclick=\"navigator.clipboard.writeText('{}'); "
148
+ "this.style.backgroundColor='#90EE90'; "
149
+ "setTimeout(() => this.style.backgroundColor='', 1000)\">{}</span>",
150
+ obj.cloudflare_id,
151
+ obj.cloudflare_id[:20] + "..."
152
+ if len(obj.cloudflare_id) > 20
153
+ else obj.cloudflare_id,
154
+ )
155
+ return "-"
156
+
157
+ cloudflare_id_display.short_description = "Cloudflare ID"
158
+
159
+ def user_display(self, obj):
160
+ """Display user with link to user admin."""
161
+ if obj.user:
162
+ url = reverse("admin:auth_user_change", args=[obj.user.pk])
163
+ return format_html('<a href="{}">{}</a>', url, obj.user.username)
164
+ return "-"
165
+
166
+ user_display.short_description = "User"
167
+
168
+ def status_display(self, obj):
169
+ """Display status with color coding."""
170
+ colors = {
171
+ ImageUploadStatus.PENDING: "#ffc107", # Yellow
172
+ ImageUploadStatus.DRAFT: "#17a2b8", # Blue
173
+ ImageUploadStatus.UPLOADED: "#28a745", # Green
174
+ ImageUploadStatus.FAILED: "#dc3545", # Red
175
+ ImageUploadStatus.EXPIRED: "#6c757d", # Gray
176
+ }
177
+ color = colors.get(obj.status, "#6c757d")
178
+ return format_html(
179
+ '<span style="color: {}; font-weight: bold;">{}</span>',
180
+ color,
181
+ obj.get_status_display(),
182
+ )
183
+
184
+ status_display.short_description = "Status"
185
+
186
+ def filename_display(self, obj):
187
+ """Display filename with truncation."""
188
+ if obj.filename:
189
+ if len(obj.filename) > 30:
190
+ return format_html(
191
+ '<span title="{}">{}</span>',
192
+ obj.filename,
193
+ obj.filename[:27] + "...",
194
+ )
195
+ return obj.filename
196
+ return obj.original_filename or "-"
197
+
198
+ filename_display.short_description = "Filename"
199
+
200
+ def file_size_display(self, obj):
201
+ """Display file size in human readable format."""
202
+ if obj.file_size:
203
+ if obj.file_size < 1024:
204
+ return f"{obj.file_size} B"
205
+ elif obj.file_size < 1024 * 1024:
206
+ return f"{obj.file_size / 1024:.1f} KB"
207
+ else:
208
+ return f"{obj.file_size / (1024 * 1024):.1f} MB"
209
+ return "-"
210
+
211
+ file_size_display.short_description = "File Size"
212
+
213
+ def is_expired_display(self, obj):
214
+ """Display expiry status with icon."""
215
+ if obj.is_expired:
216
+ return format_html('<span style="color: #dc3545;">🔴 Expired</span>')
217
+ else:
218
+ return format_html('<span style="color: #28a745;">🟢 Valid</span>')
219
+
220
+ is_expired_display.short_description = "Expiry Status"
221
+
222
+ def thumbnail_preview(self, obj):
223
+ """Display thumbnail preview if available."""
224
+ if obj.is_uploaded and obj.thumbnail_url:
225
+ return format_html(
226
+ '<img src="{}" style="max-width: 50px; max-height: 50px; border-radius: 4px;" />',
227
+ obj.thumbnail_url,
228
+ )
229
+ return "-"
230
+
231
+ thumbnail_preview.short_description = "Preview"
232
+
233
+ def actions_display(self, obj):
234
+ """Display action buttons."""
235
+ actions = []
236
+
237
+ if obj.status in [ImageUploadStatus.PENDING, ImageUploadStatus.DRAFT]:
238
+ actions.append(
239
+ format_html(
240
+ '<a href="javascript:void(0)" onclick="checkStatus(\'{}\')" '
241
+ 'style="color: #007cba; text-decoration: none;">🔄 Check</a>',
242
+ obj.pk,
243
+ )
244
+ )
245
+
246
+ if obj.is_uploaded and obj.public_url:
247
+ actions.append(
248
+ format_html(
249
+ '<a href="{}" target="_blank" style="color: #007cba; text-decoration: none;">👁️ View</a>',
250
+ obj.public_url,
251
+ )
252
+ )
253
+
254
+ return format_html(" | ".join(actions)) if actions else "-"
255
+
256
+ actions_display.short_description = "Actions"
257
+
258
+ # Readonly field methods
259
+ def upload_url_display(self, obj):
260
+ """Display upload URL with security."""
261
+ if obj.upload_url and not obj.is_expired:
262
+ return format_html(
263
+ '<div style="font-family: monospace; font-size: 11px; word-break: break-all; '
264
+ 'background: #f8f9fa; padding: 8px; border-radius: 4px; max-width: 400px;">'
265
+ "<strong>⚠️ Sensitive:</strong> {}</div>",
266
+ obj.upload_url,
267
+ )
268
+ elif obj.is_expired:
269
+ return format_html('<span style="color: #dc3545;">Expired</span>')
270
+ return "-"
271
+
272
+ upload_url_display.short_description = "Upload URL"
273
+
274
+ def variants_display(self, obj):
275
+ """Display available variants."""
276
+ if obj.variants:
277
+ variants_html = []
278
+ for variant in obj.variants:
279
+ variants_html.append(
280
+ format_html(
281
+ '<a href="{}" target="_blank" style="display: block; margin: 2px 0; '
282
+ 'font-size: 11px; color: #007cba;">{}</a>',
283
+ variant,
284
+ variant.split("/")[-1] if "/" in variant else variant,
285
+ )
286
+ )
287
+ return format_html("<div>{}</div>", "".join(variants_html))
288
+ return "-"
289
+
290
+ variants_display.short_description = "Variants"
291
+
292
+ def cloudflare_metadata_display(self, obj):
293
+ """Display Cloudflare metadata."""
294
+ if obj.cloudflare_metadata:
295
+ try:
296
+ formatted = json.dumps(obj.cloudflare_metadata, indent=2)
297
+ return format_html('<pre style="font-size: 11px;">{}</pre>', formatted)
298
+ except (TypeError, ValueError):
299
+ return str(obj.cloudflare_metadata)
300
+ return "-"
301
+
302
+ cloudflare_metadata_display.short_description = "Cloudflare Metadata"
303
+
304
+ def is_uploaded_display(self, obj):
305
+ """Display upload status."""
306
+ return "✅ Yes" if obj.is_uploaded else "❌ No"
307
+
308
+ is_uploaded_display.short_description = "Is Uploaded"
309
+
310
+ def public_url_display(self, obj):
311
+ """Display public URL with link."""
312
+ if obj.public_url:
313
+ return format_html(
314
+ '<a href="{}" target="_blank" style="font-family: monospace; font-size: 11px;">{}</a>',
315
+ obj.public_url,
316
+ obj.public_url,
317
+ )
318
+ return "-"
319
+
320
+ public_url_display.short_description = "Public URL"
321
+
322
+ def thumbnail_url_display(self, obj):
323
+ """Display thumbnail URL with link."""
324
+ if obj.thumbnail_url:
325
+ return format_html(
326
+ '<a href="{}" target="_blank" style="font-family: monospace; font-size: 11px;">{}</a>',
327
+ obj.thumbnail_url,
328
+ obj.thumbnail_url,
329
+ )
330
+ return "-"
331
+
332
+ thumbnail_url_display.short_description = "Thumbnail URL"
333
+
334
+ def image_preview(self, obj):
335
+ """Display larger image preview."""
336
+ if obj.is_uploaded and obj.public_url:
337
+ return format_html(
338
+ '<div style="text-align: center;">'
339
+ '<img src="{}" style="max-width: 300px; max-height: 200px; border: 1px solid #ddd; border-radius: 4px;" />'
340
+ "</div>",
341
+ obj.thumbnail_url or obj.public_url,
342
+ )
343
+ return "-"
344
+
345
+ image_preview.short_description = "Image Preview"
346
+
347
+ def transformation_examples(self, obj):
348
+ """Display transformation examples."""
349
+ if obj.is_uploaded and obj.public_url:
350
+ from .transformations import CloudflareImageVariants
351
+
352
+ examples = [
353
+ (
354
+ "Thumbnail 100px",
355
+ CloudflareImageVariants.thumbnail(obj.public_url, 100),
356
+ ),
357
+ ("Avatar 80px", CloudflareImageVariants.avatar(obj.public_url, 80)),
358
+ (
359
+ "Product 200px",
360
+ CloudflareImageVariants.product_image(obj.public_url, 200),
361
+ ),
362
+ ]
363
+
364
+ html_parts = []
365
+ for name, url in examples:
366
+ html_parts.append(
367
+ format_html(
368
+ '<div style="margin: 5px 0;">'
369
+ "<strong>{}:</strong><br>"
370
+ '<img src="{}" style="max-width: 80px; max-height: 80px; margin: 2px; border: 1px solid #ddd;" />'
371
+ "</div>",
372
+ name,
373
+ url,
374
+ )
375
+ )
376
+
377
+ return format_html("<div>{}</div>", "".join(html_parts))
378
+ return "-"
379
+
380
+ transformation_examples.short_description = "Transformation Examples"
381
+
382
+ # Admin actions
383
+ def check_status_action(self, request, queryset):
384
+ """Check status for selected images."""
385
+ updated_count = 0
386
+ error_count = 0
387
+
388
+ for image in queryset:
389
+ try:
390
+ cloudflare_service.check_image_status(image)
391
+ updated_count += 1
392
+ except CloudflareImagesError:
393
+ error_count += 1
394
+
395
+ if updated_count:
396
+ self.message_user(
397
+ request, f"Successfully updated status for {updated_count} images."
398
+ )
399
+ if error_count:
400
+ self.message_user(
401
+ request, f"Failed to update {error_count} images.", level="WARNING"
402
+ )
403
+
404
+ check_status_action.short_description = "Check status from Cloudflare"
405
+
406
+ def mark_as_expired(self, request, queryset):
407
+ """Mark selected images as expired."""
408
+ count = queryset.update(status=ImageUploadStatus.EXPIRED)
409
+ self.message_user(request, f"Marked {count} images as expired.")
410
+
411
+ mark_as_expired.short_description = "Mark as expired"
412
+
413
+ def delete_from_cloudflare_action(self, request, queryset):
414
+ """Delete selected images from Cloudflare."""
415
+ deleted_count = 0
416
+ error_count = 0
417
+
418
+ for image in queryset:
419
+ try:
420
+ cloudflare_service.delete_image(image)
421
+ image.delete()
422
+ deleted_count += 1
423
+ except CloudflareImagesError:
424
+ error_count += 1
425
+
426
+ if deleted_count:
427
+ self.message_user(
428
+ request, f"Successfully deleted {deleted_count} images from Cloudflare."
429
+ )
430
+ if error_count:
431
+ self.message_user(
432
+ request, f"Failed to delete {error_count} images.", level="WARNING"
433
+ )
434
+
435
+ delete_from_cloudflare_action.short_description = "Delete from Cloudflare"
436
+
437
+ def refresh_all_status(self, request, queryset):
438
+ """Refresh status for all non-final status images."""
439
+ pending_images = queryset.filter(
440
+ status__in=[ImageUploadStatus.PENDING, ImageUploadStatus.DRAFT]
441
+ )
442
+
443
+ updated_count = 0
444
+ for image in pending_images:
445
+ try:
446
+ cloudflare_service.check_image_status(image)
447
+ updated_count += 1
448
+ except CloudflareImagesError:
449
+ pass
450
+
451
+ self.message_user(request, f"Refreshed status for {updated_count} images.")
452
+
453
+ refresh_all_status.short_description = "Refresh all pending/draft status"
454
+
455
+ class Media:
456
+ js = ("admin/js/cloudflare_images_admin.js",)
457
+ css = {"all": ("admin/css/cloudflare_images_admin.css",)}
458
+
459
+
460
+ @admin.register(ImageUploadLog)
461
+ class ImageUploadLogAdmin(admin.ModelAdmin):
462
+ """Admin interface for ImageUploadLog model."""
463
+
464
+ list_display = (
465
+ "timestamp",
466
+ "image_display",
467
+ "event_type",
468
+ "message_display",
469
+ "user_display",
470
+ )
471
+
472
+ list_filter = (
473
+ "event_type",
474
+ "timestamp",
475
+ ("image__user", admin.RelatedOnlyFieldListFilter),
476
+ )
477
+
478
+ search_fields = (
479
+ "image__cloudflare_id",
480
+ "event_type",
481
+ "message",
482
+ "image__user__username",
483
+ )
484
+
485
+ readonly_fields = ("image", "event_type", "message", "formatted_data", "timestamp")
486
+
487
+ fields = ("image", "event_type", "message", "formatted_data", "timestamp")
488
+
489
+ list_per_page = 50
490
+ date_hierarchy = "timestamp"
491
+ ordering = ("-timestamp",)
492
+
493
+ def get_queryset(self, request):
494
+ """Optimize queryset."""
495
+ return super().get_queryset(request).select_related("image", "image__user")
496
+
497
+ def image_display(self, obj):
498
+ """Display image with link."""
499
+ if obj.image:
500
+ url = reverse(
501
+ "admin:django_cloudflareimages_toolkit_cloudflareimage_change",
502
+ args=[obj.image.pk],
503
+ )
504
+ return format_html(
505
+ '<a href="{}">{}</a>',
506
+ url,
507
+ obj.image.cloudflare_id[:20] + "..."
508
+ if len(obj.image.cloudflare_id) > 20
509
+ else obj.image.cloudflare_id,
510
+ )
511
+ return "-"
512
+
513
+ image_display.short_description = "Image"
514
+
515
+ def message_display(self, obj):
516
+ """Display message with truncation."""
517
+ if len(obj.message) > 50:
518
+ return format_html(
519
+ '<span title="{}">{}</span>', obj.message, obj.message[:47] + "..."
520
+ )
521
+ return obj.message
522
+
523
+ message_display.short_description = "Message"
524
+
525
+ def user_display(self, obj):
526
+ """Display user."""
527
+ if obj.image and obj.image.user:
528
+ return obj.image.user.username
529
+ return "-"
530
+
531
+ user_display.short_description = "User"
532
+
533
+ def formatted_data(self, obj):
534
+ """Format JSON data for display."""
535
+ if obj.data:
536
+ try:
537
+ formatted = json.dumps(obj.data, indent=2)
538
+ return format_html(
539
+ '<pre style="font-size: 11px; max-height: 200px; overflow-y: auto;">{}</pre>',
540
+ formatted,
541
+ )
542
+ except (TypeError, ValueError):
543
+ return str(obj.data)
544
+ return "-"
545
+
546
+ formatted_data.short_description = "Data"
547
+
548
+
549
+ # Custom admin site configuration
550
+ class CloudflareImagesAdminSite(admin.AdminSite):
551
+ """Custom admin site for Cloudflare Images."""
552
+
553
+ site_header = "Cloudflare Images Administration"
554
+ site_title = "Cloudflare Images Admin"
555
+ index_title = "Cloudflare Images Management"
556
+
557
+ def index(self, request, extra_context=None):
558
+ """Custom index with statistics."""
559
+ extra_context = extra_context or {}
560
+
561
+ # Get statistics
562
+ total_images = CloudflareImage.objects.count()
563
+ uploaded_images = CloudflareImage.objects.filter(
564
+ status=ImageUploadStatus.UPLOADED
565
+ ).count()
566
+ pending_images = CloudflareImage.objects.filter(
567
+ status=ImageUploadStatus.PENDING
568
+ ).count()
569
+ expired_images = CloudflareImage.objects.filter(
570
+ expires_at__lt=timezone.now()
571
+ ).count()
572
+
573
+ # Recent activity
574
+ recent_uploads = CloudflareImage.objects.filter(
575
+ uploaded_at__isnull=False
576
+ ).order_by("-uploaded_at")[:5]
577
+
578
+ recent_logs = ImageUploadLog.objects.select_related("image").order_by(
579
+ "-timestamp"
580
+ )[:10]
581
+
582
+ extra_context.update(
583
+ {
584
+ "cloudflare_stats": {
585
+ "total_images": total_images,
586
+ "uploaded_images": uploaded_images,
587
+ "pending_images": pending_images,
588
+ "expired_images": expired_images,
589
+ "upload_success_rate": (uploaded_images / total_images * 100)
590
+ if total_images > 0
591
+ else 0,
592
+ },
593
+ "recent_uploads": recent_uploads,
594
+ "recent_logs": recent_logs,
595
+ }
596
+ )
597
+
598
+ return super().index(request, extra_context)
599
+
600
+
601
+ # Register with custom admin site if desired
602
+ # cloudflare_admin_site = CloudflareImagesAdminSite(name='cloudflare_admin')
603
+ # cloudflare_admin_site.register(CloudflareImage, CloudflareImageAdmin)
604
+ # cloudflare_admin_site.register(ImageUploadLog, ImageUploadLogAdmin)
@@ -0,0 +1,18 @@
1
+ """
2
+ Django app configuration for Cloudflare Images Toolkit.
3
+ """
4
+
5
+ from django.apps import AppConfig
6
+
7
+
8
+ class CloudflareImagesConfig(AppConfig):
9
+ """App configuration for django_cloudflareimages_toolkit."""
10
+
11
+ default_auto_field = "django.db.models.BigAutoField"
12
+ name = "django_cloudflareimages_toolkit"
13
+ verbose_name = "Cloudflare Images Toolkit"
14
+
15
+ def ready(self):
16
+ """Initialize the app when Django starts."""
17
+ # Import signal handlers if any
18
+ pass
@@ -0,0 +1,3 @@
1
+ """
2
+ Management commands for Cloudflare Images.
3
+ """
@@ -0,0 +1,3 @@
1
+ """
2
+ Management commands for Cloudflare Images.
3
+ """