diff --git a/myproject/products/admin.py b/myproject/products/admin.py
index 3b8ac3d..77cae34 100644
--- a/myproject/products/admin.py
+++ b/myproject/products/admin.py
@@ -1,11 +1,159 @@
from django.contrib import admin
from django.utils.html import format_html
+from django.utils import timezone
import nested_admin
from .models import ProductCategory, ProductTag, Product, ProductKit, KitItem
from .models import ProductPhoto, ProductKitPhoto, ProductCategoryPhoto
from .models import ProductVariantGroup, KitItemPriority, SKUCounter
+class DeletedFilter(admin.SimpleListFilter):
+ """Фильтр для отображения удаленных/активных элементов"""
+ title = 'Статус удаления'
+ parameter_name = 'is_deleted'
+
+ def lookups(self, request, model_admin):
+ return (
+ ('0', 'Активные'),
+ ('1', 'Удаленные'),
+ )
+
+ def queryset(self, request, queryset):
+ # queryset уже содержит всё (включая удаленные) благодаря get_queryset()
+ if self.value() == '0':
+ return queryset.filter(is_deleted=False)
+ elif self.value() == '1':
+ return queryset.filter(is_deleted=True)
+ return queryset
+
+
+def restore_items(modeladmin, request, queryset):
+ """Action для восстановления удаленных элементов"""
+ updated = queryset.update(is_deleted=False, deleted_at=None, deleted_by=None)
+ modeladmin.message_user(request, f'✓ Восстановлено {updated} элемент(ов).')
+restore_items.short_description = '✓ Восстановить выбранные элементы'
+
+
+def delete_selected(modeladmin, request, queryset):
+ """
+ Переопределенный action удаления который вызывает .delete() на каждый объект
+ чтобы гарантировать мягкое удаление вместо hard delete
+ """
+ count = 0
+ for obj in queryset:
+ obj.delete() # Вызывает delete() метод модели (мягкое удаление)
+ count += 1
+ modeladmin.message_user(request, f'✓ Удалено {count} элемент(ов).')
+delete_selected.short_description = '🗑️ Удалить выбранные элементы'
+
+
+def hard_delete_selected(modeladmin, request, queryset):
+ """
+ Action для БЕЗОПАСНОГО полного удаления из БД.
+ Удаляет объект только если у него нет опасных связей.
+
+ БЕЗОПАСНЫЕ связи (удаляются вместе с объектом):
+ - photos: фотографии объекта (создаются только для этого объекта)
+
+ ОПАСНЫЕ связи (блокируют удаление):
+ - kit_items: товар используется в составе комплекта
+ - kit_items_direct: товар включен как компонент комплекта
+ - ManyToMany: товар связан с другими объектами
+ """
+ from django.contrib import messages
+
+ items_to_delete = []
+ items_with_relations = []
+
+ # Связи которые БЕЗОПАСНО удалять вместе с объектом (они зависят от объекта)
+ SAFE_RELATIONS = {'photos'}
+
+ for obj in queryset:
+ # Проверяем есть ли опасные связанные объекты
+ try:
+ has_dangerous_relations = False
+ dangerous_relation_details = []
+
+ for relation in obj._meta.get_fields():
+ # Пропускаем обычные поля
+ if not hasattr(relation, 'related_model'):
+ continue
+
+ # Для reverse relations получаем count
+ if hasattr(relation, 'get_accessor_name'):
+ try:
+ accessor_name = relation.get_accessor_name()
+
+ # Пропускаем безопасные связи (удалятся при cascade delete)
+ if accessor_name in SAFE_RELATIONS:
+ continue
+
+ related_manager = getattr(obj, accessor_name, None)
+ if related_manager and hasattr(related_manager, 'count'):
+ count = related_manager.count()
+ if count > 0:
+ has_dangerous_relations = True
+ relation_name = relation.related_model.__name__
+ dangerous_relation_details.append(f"{relation_name} ({count})")
+ except (AttributeError, Exception):
+ # Пропускаем ошибки доступа к связям
+ pass
+
+ if has_dangerous_relations:
+ details_str = ', '.join(dangerous_relation_details)
+ items_with_relations.append(f"{str(obj)} [{details_str}]")
+ else:
+ items_to_delete.append(obj)
+ except Exception as e:
+ # На случай любой неожиданной ошибки при проверке
+ items_with_relations.append(f"{str(obj)} (ошибка проверки: {str(e)})")
+
+ # Удаляем безопасные элементы
+ deleted_count = 0
+ for obj in items_to_delete:
+ try:
+ # ВАЖНО: Сначала явно удаляем все фотографии
+ # Это гарантирует что delete() метод каждого фото вызовется
+ # и удалит файлы из media/ папки
+ # Нужно для всех моделей которые имеют 'photos' связь
+
+ # Для Product/ProductKit/ProductCategory - есть .photos
+ if hasattr(obj, 'photos'):
+ photos = list(obj.photos.all()) # Получаем копию списка перед удалением
+ for photo in photos:
+ photo.delete() # Вызывает ProductPhoto.delete() который удалит файлы
+
+ # Потом удаляем сам объект
+ obj.hard_delete()
+ deleted_count += 1
+ except Exception as e:
+ items_with_relations.append(f"{str(obj)} (ошибка удаления: {str(e)})")
+
+ # Выводим результаты
+ if deleted_count > 0:
+ modeladmin.message_user(
+ request,
+ f'✓ Безопасно удалено {deleted_count} элемент(ов) (с фотографиями из media папки).',
+ messages.SUCCESS
+ )
+
+ if items_with_relations:
+ error_msg = f'⚠️ Не удалось удалить {len(items_with_relations)} элемент(ов) (имеют зависимые связи):\n' + '\n'.join(f' • {item}' for item in items_with_relations)
+ modeladmin.message_user(request, error_msg, messages.WARNING)
+
+hard_delete_selected.short_description = '🔴 Безопасно удалить навсегда (только без связей)'
+
+
+def disable_delete_selected(admin_class):
+ """
+ Декоратор для отключения стандартного Django delete_selected action.
+ Наш custom delete_selected будет использоваться вместо него.
+ """
+ admin_class.actions = [action for action in admin_class.actions
+ if action.__name__ != 'delete_selected']
+ return admin_class
+
+
@admin.register(ProductVariantGroup)
class ProductVariantGroupAdmin(admin.ModelAdmin):
list_display = ['name', 'get_products_count', 'created_at']
@@ -19,11 +167,29 @@ class ProductVariantGroupAdmin(admin.ModelAdmin):
class ProductCategoryAdmin(admin.ModelAdmin):
- list_display = ('photo_preview', 'name', 'sku', 'slug', 'parent', 'is_active')
- list_filter = ('is_active', 'parent')
+ list_display = ('photo_preview', 'name', 'sku', 'slug', 'parent', 'is_active', 'get_deleted_status')
+ list_filter = (DeletedFilter, 'is_active', 'parent')
prepopulated_fields = {'slug': ('name',)}
search_fields = ('name', 'sku')
- readonly_fields = ('photo_preview_large',)
+ readonly_fields = ('photo_preview_large', 'deleted_at', 'deleted_by')
+ actions = [restore_items, delete_selected, hard_delete_selected]
+
+ def get_queryset(self, request):
+ """Переопределяем queryset для доступа ко всем категориям (включая удаленные)"""
+ qs = ProductCategory.all_objects.all()
+ ordering = self.get_ordering(request)
+ if ordering:
+ qs = qs.order_by(*ordering)
+ return qs
+
+ def get_deleted_status(self, obj):
+ """Показывает статус удаления"""
+ if obj.is_deleted:
+ return format_html(
+ '🗑️ Удалена'
+ )
+ return format_html('✓ Активна')
+ get_deleted_status.short_description = 'Статус'
def photo_preview(self, obj):
"""Превью фото в списке категорий"""
@@ -49,18 +215,39 @@ class ProductCategoryAdmin(admin.ModelAdmin):
class ProductTagAdmin(admin.ModelAdmin):
- list_display = ('name', 'slug')
+ list_display = ('name', 'slug', 'get_deleted_status')
+ list_filter = (DeletedFilter,)
prepopulated_fields = {'slug': ('name',)}
search_fields = ('name',)
+ readonly_fields = ('deleted_at', 'deleted_by')
+ actions = [restore_items, delete_selected, hard_delete_selected]
+
+ def get_queryset(self, request):
+ """Переопределяем queryset для доступа ко всем тегам (включая удаленные)"""
+ qs = ProductTag.all_objects.all()
+ ordering = self.get_ordering(request)
+ if ordering:
+ qs = qs.order_by(*ordering)
+ return qs
+
+ def get_deleted_status(self, obj):
+ """Показывает статус удаления"""
+ if obj.is_deleted:
+ return format_html(
+ '🗑️ Удален'
+ )
+ return format_html('✓ Активен')
+ get_deleted_status.short_description = 'Статус'
class ProductAdmin(admin.ModelAdmin):
- list_display = ('photo_preview', 'name', 'sku', 'get_categories_display', 'cost_price', 'sale_price', 'get_variant_groups_display', 'is_active')
- list_filter = ('is_active', 'categories', 'tags', 'variant_groups')
+ list_display = ('photo_preview', 'name', 'sku', 'get_categories_display', 'cost_price', 'sale_price', 'get_variant_groups_display', 'is_active', 'get_deleted_status')
+ list_filter = (DeletedFilter, 'is_active', 'categories', 'tags', 'variant_groups')
search_fields = ('name', 'sku', 'description', 'search_keywords')
filter_horizontal = ('categories', 'tags', 'variant_groups')
- readonly_fields = ('photo_preview_large',)
+ readonly_fields = ('photo_preview_large', 'deleted_at', 'deleted_by')
autocomplete_fields = []
+ actions = [restore_items, delete_selected, hard_delete_selected]
fieldsets = (
('Основная информация', {
@@ -72,6 +259,11 @@ class ProductAdmin(admin.ModelAdmin):
('Дополнительно', {
'fields': ('tags', 'variant_groups', 'is_active')
}),
+ ('Удаление', {
+ 'fields': ('deleted_at', 'deleted_by'),
+ 'classes': ('collapse',),
+ 'description': 'Информация о мягком удалении товара.'
+ }),
('Поиск', {
'fields': ('search_keywords',),
'classes': ('collapse',),
@@ -83,6 +275,23 @@ class ProductAdmin(admin.ModelAdmin):
}),
)
+ def get_queryset(self, request):
+ """Переопределяем queryset для доступа ко всем товарам (включая удаленные)"""
+ qs = Product.all_objects.all()
+ ordering = self.get_ordering(request)
+ if ordering:
+ qs = qs.order_by(*ordering)
+ return qs
+
+ def get_deleted_status(self, obj):
+ """Показывает статус удаления"""
+ if obj.is_deleted:
+ return format_html(
+ '🗑️ Удален'
+ )
+ return format_html('✓ Активен')
+ get_deleted_status.short_description = 'Статус'
+
def get_categories_display(self, obj):
categories = obj.categories.all()[:3]
if not categories:
@@ -127,11 +336,29 @@ class ProductAdmin(admin.ModelAdmin):
class ProductKitAdmin(admin.ModelAdmin):
- list_display = ('photo_preview', 'name', 'slug', 'get_categories_display', 'pricing_method', 'is_active')
- list_filter = ('is_active', 'pricing_method', 'categories', 'tags')
+ list_display = ('photo_preview', 'name', 'slug', 'get_categories_display', 'pricing_method', 'is_active', 'get_deleted_status')
+ list_filter = (DeletedFilter, 'is_active', 'pricing_method', 'categories', 'tags')
prepopulated_fields = {'slug': ('name',)}
filter_horizontal = ('categories', 'tags')
- readonly_fields = ('photo_preview_large',)
+ readonly_fields = ('photo_preview_large', 'deleted_at', 'deleted_by')
+ actions = [restore_items, delete_selected, hard_delete_selected]
+
+ def get_queryset(self, request):
+ """Переопределяем queryset для доступа ко всем комплектам (включая удаленные)"""
+ qs = ProductKit.all_objects.all()
+ ordering = self.get_ordering(request)
+ if ordering:
+ qs = qs.order_by(*ordering)
+ return qs
+
+ def get_deleted_status(self, obj):
+ """Показывает статус удаления"""
+ if obj.is_deleted:
+ return format_html(
+ '🗑️ Удален'
+ )
+ return format_html('✓ Активен')
+ get_deleted_status.short_description = 'Статус'
def get_categories_display(self, obj):
categories = obj.categories.all()[:3]
diff --git a/myproject/products/models.py b/myproject/products/models.py
index 0ece353..75fd662 100644
--- a/myproject/products/models.py
+++ b/myproject/products/models.py
@@ -3,9 +3,14 @@ from django.urls import reverse
from django.utils.text import slugify
from django.core.exceptions import ValidationError
from django.db import transaction
+from django.utils import timezone
+from django.contrib.auth import get_user_model
from .utils.sku_generator import generate_product_sku, generate_kit_sku, generate_category_sku
+# Получаем User модель один раз для использования в ForeignKey
+User = get_user_model()
+
class SKUCounter(models.Model):
"""
@@ -57,6 +62,60 @@ class ActiveManager(models.Manager):
return super().get_queryset().filter(is_active=True)
+class SoftDeleteQuerySet(models.QuerySet):
+ """
+ QuerySet для мягкого удаления (soft delete).
+ Позволяет фильтровать удаленные элементы и восстанавливать их.
+ """
+ def delete(self):
+ """Soft delete вместо hard delete"""
+ return self.update(
+ is_deleted=True,
+ deleted_at=timezone.now()
+ )
+
+ def hard_delete(self):
+ """Явный hard delete - удаляет из БД окончательно"""
+ return super().delete()
+
+ def restore(self):
+ """Восстановление из удаленного состояния"""
+ return self.update(
+ is_deleted=False,
+ deleted_at=None,
+ deleted_by=None
+ )
+
+ def deleted_only(self):
+ """Получить только удаленные элементы"""
+ return self.filter(is_deleted=True)
+
+ def not_deleted(self):
+ """Получить только не удаленные элементы"""
+ return self.filter(is_deleted=False)
+
+ def with_deleted(self):
+ """Получить все элементы включая удаленные"""
+ return self.all()
+
+
+class SoftDeleteManager(models.Manager):
+ """
+ Manager для работы с мягким удалением.
+ По умолчанию исключает удаленные элементы из запросов.
+ """
+ def get_queryset(self):
+ return SoftDeleteQuerySet(self.model, using=self._db).filter(is_deleted=False)
+
+ def deleted_only(self):
+ """Получить только удаленные элементы"""
+ return SoftDeleteQuerySet(self.model, using=self._db).filter(is_deleted=True)
+
+ def all_with_deleted(self):
+ """Получить все элементы включая удаленные"""
+ return SoftDeleteQuerySet(self.model, using=self._db).all()
+
+
class ProductCategory(models.Model):
"""
Категории товаров и комплектов (поддержка нескольких уровней не обязательна, но возможна позже).
@@ -67,8 +126,23 @@ class ProductCategory(models.Model):
parent = models.ForeignKey('self', on_delete=models.SET_NULL, null=True, blank=True,
related_name='children', verbose_name="Родительская категория")
is_active = models.BooleanField(default=True, verbose_name="Активна")
+ created_at = models.DateTimeField(auto_now_add=True, verbose_name="Дата создания", null=True)
+ updated_at = models.DateTimeField(auto_now=True, verbose_name="Дата обновления", null=True)
- objects = models.Manager() # Менеджер по умолчанию
+ # Поля для мягкого удаления
+ is_deleted = models.BooleanField(default=False, verbose_name="Удалена", db_index=True)
+ deleted_at = models.DateTimeField(null=True, blank=True, verbose_name="Время удаления")
+ deleted_by = models.ForeignKey(
+ User,
+ on_delete=models.SET_NULL,
+ null=True,
+ blank=True,
+ related_name='deleted_categories',
+ verbose_name="Удалена пользователем"
+ )
+
+ objects = SoftDeleteManager.from_queryset(SoftDeleteQuerySet)() # Менеджер по умолчанию (исключает удаленные)
+ all_objects = models.Manager() # Менеджер для доступа ко ВСЕМ объектам (включая удаленные)
active = ActiveManager() # Кастомный менеджер для активных категорий
class Meta:
@@ -76,6 +150,8 @@ class ProductCategory(models.Model):
verbose_name_plural = "Категории товаров"
indexes = [
models.Index(fields=['is_active']),
+ models.Index(fields=['is_deleted']),
+ models.Index(fields=['is_deleted', 'created_at']),
]
def __str__(self):
@@ -144,6 +220,18 @@ class ProductCategory(models.Model):
super().save(*args, **kwargs)
+ def delete(self, *args, **kwargs):
+ """Soft delete вместо hard delete - марк как удаленный"""
+ self.is_deleted = True
+ self.deleted_at = timezone.now()
+ self.save(update_fields=['is_deleted', 'deleted_at'])
+ # Возвращаем результат в формате Django
+ return 1, {self.__class__._meta.label: 1}
+
+ def hard_delete(self):
+ """Полное удаление из БД (необратимо!)"""
+ super().delete()
+
class ProductTag(models.Model):
"""
@@ -151,10 +239,31 @@ class ProductTag(models.Model):
"""
name = models.CharField(max_length=100, unique=True, verbose_name="Название")
slug = models.SlugField(max_length=100, unique=True, verbose_name="URL-идентификатор")
+ created_at = models.DateTimeField(auto_now_add=True, verbose_name="Дата создания", null=True)
+ updated_at = models.DateTimeField(auto_now=True, verbose_name="Дата обновления", null=True)
+
+ # Поля для мягкого удаления
+ is_deleted = models.BooleanField(default=False, verbose_name="Удален", db_index=True)
+ deleted_at = models.DateTimeField(null=True, blank=True, verbose_name="Время удаления")
+ deleted_by = models.ForeignKey(
+ User,
+ on_delete=models.SET_NULL,
+ null=True,
+ blank=True,
+ related_name='deleted_tags',
+ verbose_name="Удален пользователем"
+ )
+
+ objects = SoftDeleteManager.from_queryset(SoftDeleteQuerySet)() # Менеджер по умолчанию (исключает удаленные)
+ all_objects = models.Manager() # Менеджер для доступа ко ВСЕМ объектам (включая удаленные)
class Meta:
verbose_name = "Тег товара"
verbose_name_plural = "Теги товаров"
+ indexes = [
+ models.Index(fields=['is_deleted']),
+ models.Index(fields=['is_deleted', 'created_at']),
+ ]
def __str__(self):
return self.name
@@ -164,6 +273,17 @@ class ProductTag(models.Model):
self.slug = slugify(self.name)
super().save(*args, **kwargs)
+ def delete(self, *args, **kwargs):
+ """Soft delete вместо hard delete - марк как удаленный"""
+ self.is_deleted = True
+ self.deleted_at = timezone.now()
+ self.save(update_fields=['is_deleted', 'deleted_at'])
+ return 1, {self.__class__._meta.label: 1}
+
+ def hard_delete(self):
+ """Полное удаление из БД (необратимо!)"""
+ super().delete()
+
class ProductVariantGroup(models.Model):
"""
@@ -237,7 +357,20 @@ class Product(models.Model):
help_text="Автоматически генерируется из названия, артикула, описания и категории. Можно дополнить вручную."
)
- objects = models.Manager() # Менеджер по умолчанию
+ # Поля для мягкого удаления
+ is_deleted = models.BooleanField(default=False, verbose_name="Удален", db_index=True)
+ deleted_at = models.DateTimeField(null=True, blank=True, verbose_name="Время удаления")
+ deleted_by = models.ForeignKey(
+ User,
+ on_delete=models.SET_NULL,
+ null=True,
+ blank=True,
+ related_name='deleted_products',
+ verbose_name="Удален пользователем"
+ )
+
+ objects = SoftDeleteManager.from_queryset(SoftDeleteQuerySet)() # Менеджер по умолчанию (исключает удаленные)
+ all_objects = models.Manager() # Менеджер для доступа ко ВСЕМ объектам (включая удаленные)
active = ActiveManager() # Кастомный менеджер для активных товаров
class Meta:
@@ -245,6 +378,8 @@ class Product(models.Model):
verbose_name_plural = "Товары"
indexes = [
models.Index(fields=['is_active']),
+ models.Index(fields=['is_deleted']),
+ models.Index(fields=['is_deleted', 'created_at']),
]
def __str__(self):
@@ -287,6 +422,18 @@ class Product(models.Model):
# Используем update чтобы избежать рекурсии
Product.objects.filter(pk=self.pk).update(search_keywords=self.search_keywords)
+ def delete(self, *args, **kwargs):
+ """Soft delete вместо hard delete - марк как удаленный"""
+ self.is_deleted = True
+ self.deleted_at = timezone.now()
+ self.save(update_fields=['is_deleted', 'deleted_at'])
+ # Возвращаем результат в формате Django
+ return 1, {self.__class__._meta.label: 1}
+
+ def hard_delete(self):
+ """Полное удаление из БД (необратимо!)"""
+ super().delete()
+
def get_variant_groups(self):
"""Возвращает все группы вариантов товара"""
return self.variant_groups.all()
@@ -332,7 +479,20 @@ class ProductKit(models.Model):
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Дата создания")
updated_at = models.DateTimeField(auto_now=True, verbose_name="Дата обновления")
- objects = models.Manager() # Менеджер по умолчанию
+ # Поля для мягкого удаления
+ is_deleted = models.BooleanField(default=False, verbose_name="Удален", db_index=True)
+ deleted_at = models.DateTimeField(null=True, blank=True, verbose_name="Время удаления")
+ deleted_by = models.ForeignKey(
+ User,
+ on_delete=models.SET_NULL,
+ null=True,
+ blank=True,
+ related_name='deleted_kits',
+ verbose_name="Удален пользователем"
+ )
+
+ objects = SoftDeleteManager.from_queryset(SoftDeleteQuerySet)() # Менеджер по умолчанию (исключает удаленные)
+ all_objects = models.Manager() # Менеджер для доступа ко ВСЕМ объектам (включая удаленные)
active = ActiveManager() # Кастомный менеджер для активных комплектов
class Meta:
@@ -341,6 +501,8 @@ class ProductKit(models.Model):
indexes = [
models.Index(fields=['is_active']),
models.Index(fields=['slug']),
+ models.Index(fields=['is_deleted']),
+ models.Index(fields=['is_deleted', 'created_at']),
]
def __str__(self):
@@ -437,6 +599,18 @@ class ProductKit(models.Model):
return total_sale
+ def delete(self, *args, **kwargs):
+ """Soft delete вместо hard delete - марк как удаленный"""
+ self.is_deleted = True
+ self.deleted_at = timezone.now()
+ self.save(update_fields=['is_deleted', 'deleted_at'])
+ # Возвращаем результат в формате Django
+ return 1, {self.__class__._meta.label: 1}
+
+ def hard_delete(self):
+ """Полное удаление из БД (необратимо!)"""
+ super().delete()
+
class KitItem(models.Model):
"""
@@ -600,8 +774,10 @@ class ProductPhoto(models.Model):
# Если было загружено новое изображение
if self.image and (is_new or old_image_path):
- # Обрабатываем изображение и получаем путь к оригиналу
- processed_paths = ImageProcessor.process_image(self.image, 'products')
+ # Обрабатываем изображение с использованием SKU товара как идентификатора
+ # SKU гарантирует уникальность и читаемость имени файла
+ identifier = self.product.sku if self.product.sku else self.product.slug
+ processed_paths = ImageProcessor.process_image(self.image, 'products', identifier=identifier)
# Сохраняем только путь к оригиналу в поле image
self.image = processed_paths['original']
@@ -613,10 +789,18 @@ class ProductPhoto(models.Model):
def delete(self, *args, **kwargs):
"""Удаляет все версии изображения при удалении фото"""
+ import logging
from .utils.image_processor import ImageProcessor
+ logger = logging.getLogger(__name__)
+
if self.image:
- ImageProcessor.delete_all_versions('products', self.image.name)
+ try:
+ logger.info(f"[ProductPhoto.delete] Удаляем изображение: {self.image.name}")
+ ImageProcessor.delete_all_versions('products', self.image.name)
+ logger.info(f"[ProductPhoto.delete] ✓ Все версии изображения удалены")
+ except Exception as e:
+ logger.error(f"[ProductPhoto.delete] ✗ Ошибка при удалении версий: {str(e)}", exc_info=True)
super().delete(*args, **kwargs)
@@ -680,8 +864,10 @@ class ProductKitPhoto(models.Model):
# Если было загружено новое изображение
if self.image and (is_new or old_image_path):
- # Обрабатываем изображение и получаем путь к оригиналу
- processed_paths = ImageProcessor.process_image(self.image, 'kits')
+ # Обрабатываем изображение с использованием slug комплекта как идентификатора
+ # slug гарантирует уникальность и читаемость имени файла
+ identifier = self.kit.slug
+ processed_paths = ImageProcessor.process_image(self.image, 'kits', identifier=identifier)
# Сохраняем только путь к оригиналу в поле image
self.image = processed_paths['original']
@@ -693,10 +879,18 @@ class ProductKitPhoto(models.Model):
def delete(self, *args, **kwargs):
"""Удаляет все версии изображения при удалении фото"""
+ import logging
from .utils.image_processor import ImageProcessor
+ logger = logging.getLogger(__name__)
+
if self.image:
- ImageProcessor.delete_all_versions('kits', self.image.name)
+ try:
+ logger.info(f"[ProductKitPhoto.delete] Удаляем изображение: {self.image.name}")
+ ImageProcessor.delete_all_versions('kits', self.image.name)
+ logger.info(f"[ProductKitPhoto.delete] ✓ Все версии изображения удалены")
+ except Exception as e:
+ logger.error(f"[ProductKitPhoto.delete] ✗ Ошибка при удалении версий: {str(e)}", exc_info=True)
super().delete(*args, **kwargs)
@@ -760,8 +954,10 @@ class ProductCategoryPhoto(models.Model):
# Если было загружено новое изображение
if self.image and (is_new or old_image_path):
- # Обрабатываем изображение и получаем путь к оригиналу
- processed_paths = ImageProcessor.process_image(self.image, 'categories')
+ # Обрабатываем изображение с использованием slug категории как идентификатора
+ # slug гарантирует уникальность и читаемость имени файла
+ identifier = self.category.slug
+ processed_paths = ImageProcessor.process_image(self.image, 'categories', identifier=identifier)
# Сохраняем только путь к оригиналу в поле image
self.image = processed_paths['original']
@@ -773,10 +969,18 @@ class ProductCategoryPhoto(models.Model):
def delete(self, *args, **kwargs):
"""Удаляет все версии изображения при удалении фото"""
+ import logging
from .utils.image_processor import ImageProcessor
+ logger = logging.getLogger(__name__)
+
if self.image:
- ImageProcessor.delete_all_versions('categories', self.image.name)
+ try:
+ logger.info(f"[ProductCategoryPhoto.delete] Удаляем изображение: {self.image.name}")
+ ImageProcessor.delete_all_versions('categories', self.image.name)
+ logger.info(f"[ProductCategoryPhoto.delete] ✓ Все версии изображения удалены")
+ except Exception as e:
+ logger.error(f"[ProductCategoryPhoto.delete] ✗ Ошибка при удалении версий: {str(e)}", exc_info=True)
super().delete(*args, **kwargs)