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:
2025-10-22 16:09:15 +03:00
parent 85801c6c4a
commit 2b6acc5564
16 changed files with 2010 additions and 74 deletions

View File

@@ -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]