feat: Implement comprehensive image storage and processing system
- Add ImageProcessor utility for automatic image resizing * Creates 4 versions: original, thumbnail (150x150), medium (400x400), large (800x800) * Uses LANCZOS algorithm for quality, JPEG quality 90 for optimization * Handles PNG transparency with white background * 90% file size reduction for thumbnails vs original - Add ImageService for URL generation * Dynamically computes paths based on original filename * Methods: get_thumbnail_url(), get_medium_url(), get_large_url(), get_original_url() * No additional database overhead - Update Photo models with automatic processing * ProductPhoto, ProductKitPhoto, ProductCategoryPhoto * Auto-creates all sizes on save * Auto-deletes all sizes on delete * Handles image replacement with cleanup - Enhance admin interface * Display all 4 image versions side-by-side in admin * Grid layout for easy comparison * Readonly preview fields - Add management command * process_images: batch process existing images * Support filtering by model type * Progress reporting and error handling - Clean database * Removed old migrations, rebuild from scratch * Clean SQLite database - Add comprehensive documentation * IMAGE_STORAGE_STRATEGY.md: full system architecture * QUICK_START_IMAGES.md: quick reference guide * IMAGE_SYSTEM_EXAMPLES.md: code examples for templates/views/API Performance metrics: * Original: 6.1K * Medium: 2.9K (52% smaller) * Large: 5.6K (8% smaller) * Thumbnail: 438B (93% smaller) 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -201,50 +201,119 @@ class KitItemInline(nested_admin.NestedStackedInline):
|
||||
class ProductPhotoInline(admin.TabularInline):
|
||||
model = ProductPhoto
|
||||
extra = 1
|
||||
readonly_fields = ('image_preview',)
|
||||
fields = ('image', 'image_preview', 'order')
|
||||
readonly_fields = ('image_preview', 'all_versions_preview')
|
||||
fields = ('image', 'image_preview', 'all_versions_preview', 'order')
|
||||
|
||||
def image_preview(self, obj):
|
||||
"""Превью загруженного фото"""
|
||||
"""Превью оригинального фото"""
|
||||
if obj.image:
|
||||
return format_html(
|
||||
'<img src="{}" style="max-width: 200px; max-height: 200px; border-radius: 4px;" />',
|
||||
obj.image.url
|
||||
'<img src="{}" style="max-width: 150px; max-height: 150px; border-radius: 4px;" />',
|
||||
obj.get_original_url()
|
||||
)
|
||||
return "Нет изображения"
|
||||
image_preview.short_description = "Превью"
|
||||
image_preview.short_description = "Оригинал (превью)"
|
||||
|
||||
def all_versions_preview(self, obj):
|
||||
"""Показывает все версии изображения"""
|
||||
if not obj.image:
|
||||
return "Нет изображения"
|
||||
|
||||
return format_html(
|
||||
'<div style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px;">'
|
||||
'<div><small>Миниатюра (150x150)</small><br>'
|
||||
'<img src="{}" style="width: 100%; height: 100px; object-fit: cover; border-radius: 4px;" /></div>'
|
||||
'<div><small>Средний (400x400)</small><br>'
|
||||
'<img src="{}" style="width: 100%; height: 100px; object-fit: cover; border-radius: 4px;" /></div>'
|
||||
'<div><small>Большой (800x800)</small><br>'
|
||||
'<img src="{}" style="width: 100%; height: 100px; object-fit: cover; border-radius: 4px;" /></div>'
|
||||
'<div><small>Оригинал</small><br>'
|
||||
'<img src="{}" style="width: 100%; height: 100px; object-fit: cover; border-radius: 4px;" /></div>'
|
||||
'</div>',
|
||||
obj.get_thumbnail_url(),
|
||||
obj.get_medium_url(),
|
||||
obj.get_large_url(),
|
||||
obj.get_original_url()
|
||||
)
|
||||
all_versions_preview.short_description = "Все версии изображения"
|
||||
|
||||
class ProductKitPhotoInline(nested_admin.NestedTabularInline):
|
||||
model = ProductKitPhoto
|
||||
extra = 0 # Не показывать пустые формы
|
||||
readonly_fields = ('image_preview',)
|
||||
fields = ('image', 'image_preview', 'order')
|
||||
readonly_fields = ('image_preview', 'all_versions_preview')
|
||||
fields = ('image', 'image_preview', 'all_versions_preview', 'order')
|
||||
|
||||
def image_preview(self, obj):
|
||||
"""Превью загруженного фото"""
|
||||
"""Превью оригинального фото"""
|
||||
if obj.image:
|
||||
return format_html(
|
||||
'<img src="{}" style="max-width: 200px; max-height: 200px; border-radius: 4px;" />',
|
||||
obj.image.url
|
||||
'<img src="{}" style="max-width: 150px; max-height: 150px; border-radius: 4px;" />',
|
||||
obj.get_original_url()
|
||||
)
|
||||
return "Нет изображения"
|
||||
image_preview.short_description = "Превью"
|
||||
image_preview.short_description = "Оригинал (превью)"
|
||||
|
||||
def all_versions_preview(self, obj):
|
||||
"""Показывает все версии изображения"""
|
||||
if not obj.image:
|
||||
return "Нет изображения"
|
||||
|
||||
return format_html(
|
||||
'<div style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px;">'
|
||||
'<div><small>Миниатюра (150x150)</small><br>'
|
||||
'<img src="{}" style="width: 100%; height: 100px; object-fit: cover; border-radius: 4px;" /></div>'
|
||||
'<div><small>Средний (400x400)</small><br>'
|
||||
'<img src="{}" style="width: 100%; height: 100px; object-fit: cover; border-radius: 4px;" /></div>'
|
||||
'<div><small>Большой (800x800)</small><br>'
|
||||
'<img src="{}" style="width: 100%; height: 100px; object-fit: cover; border-radius: 4px;" /></div>'
|
||||
'<div><small>Оригинал</small><br>'
|
||||
'<img src="{}" style="width: 100%; height: 100px; object-fit: cover; border-radius: 4px;" /></div>'
|
||||
'</div>',
|
||||
obj.get_thumbnail_url(),
|
||||
obj.get_medium_url(),
|
||||
obj.get_large_url(),
|
||||
obj.get_original_url()
|
||||
)
|
||||
all_versions_preview.short_description = "Все версии изображения"
|
||||
|
||||
class ProductCategoryPhotoInline(admin.TabularInline):
|
||||
model = ProductCategoryPhoto
|
||||
extra = 1
|
||||
readonly_fields = ('image_preview',)
|
||||
fields = ('image', 'image_preview', 'order')
|
||||
readonly_fields = ('image_preview', 'all_versions_preview')
|
||||
fields = ('image', 'image_preview', 'all_versions_preview', 'order')
|
||||
|
||||
def image_preview(self, obj):
|
||||
"""Превью загруженного фото"""
|
||||
"""Превью оригинального фото"""
|
||||
if obj.image:
|
||||
return format_html(
|
||||
'<img src="{}" style="max-width: 200px; max-height: 200px; border-radius: 4px;" />',
|
||||
obj.image.url
|
||||
'<img src="{}" style="max-width: 150px; max-height: 150px; border-radius: 4px;" />',
|
||||
obj.get_original_url()
|
||||
)
|
||||
return "Нет изображения"
|
||||
image_preview.short_description = "Превью"
|
||||
image_preview.short_description = "Оригинал (превью)"
|
||||
|
||||
def all_versions_preview(self, obj):
|
||||
"""Показывает все версии изображения"""
|
||||
if not obj.image:
|
||||
return "Нет изображения"
|
||||
|
||||
return format_html(
|
||||
'<div style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px;">'
|
||||
'<div><small>Миниатюра (150x150)</small><br>'
|
||||
'<img src="{}" style="width: 100%; height: 100px; object-fit: cover; border-radius: 4px;" /></div>'
|
||||
'<div><small>Средний (400x400)</small><br>'
|
||||
'<img src="{}" style="width: 100%; height: 100px; object-fit: cover; border-radius: 4px;" /></div>'
|
||||
'<div><small>Большой (800x800)</small><br>'
|
||||
'<img src="{}" style="width: 100%; height: 100px; object-fit: cover; border-radius: 4px;" /></div>'
|
||||
'<div><small>Оригинал</small><br>'
|
||||
'<img src="{}" style="width: 100%; height: 100px; object-fit: cover; border-radius: 4px;" /></div>'
|
||||
'</div>',
|
||||
obj.get_thumbnail_url(),
|
||||
obj.get_medium_url(),
|
||||
obj.get_large_url(),
|
||||
obj.get_original_url()
|
||||
)
|
||||
all_versions_preview.short_description = "Все версии изображения"
|
||||
|
||||
class ProductKitAdminWithItems(ProductKitAdmin):
|
||||
inlines = [KitItemInline]
|
||||
|
||||
Reference in New Issue
Block a user