""" Модели для работы с фотографиями продуктов, комплектов и категорий. Использует паттерн Template Method для устранения дублирования кода. """ from abc import abstractmethod from django.db import models from django.utils import timezone class BasePhoto(models.Model): """ Абстрактный базовый класс для всех фотомоделей. Объединяет общую логику обработки изображений для ProductPhoto, ProductKitPhoto, ProductCategoryPhoto. Паттерн: Template Method - Общие методы save(), delete() и get_*_url() определены здесь - Специфичные детали (related entity, upload path) задаются через абстрактные методы """ image = models.ImageField(verbose_name="Оригинальное фото") order = models.PositiveIntegerField(default=0, verbose_name="Порядок") created_at = models.DateTimeField(auto_now_add=True, verbose_name="Дата создания") class Meta: abstract = True ordering = ['order', '-created_at'] @abstractmethod def get_entity(self): """ Возвращает связанную сущность (product, kit, category). Должен быть реализован в дочернем классе. Returns: Model: Связанный объект (Product, ProductKit или ProductCategory) """ pass @abstractmethod def get_entity_type(self): """ Возвращает тип сущности для путей изображений. Returns: str: Одно из значений: 'products', 'kits', 'categories' """ pass def save(self, *args, **kwargs): """ При загрузке нового изображения обрабатывает его и создает все необходимые размеры. Автоматически определяет и сохраняет уровень качества (quality_level и quality_warning). """ from ..utils.image_processor import ImageProcessor is_new = not self.pk # Если это новый объект с изображением, нужно сначала сохранить без изображения, чтобы получить ID if is_new and self.image: # Сохраняем объект без изображения, чтобы получить ID temp_image = self.image self.image = None super().save(*args, **kwargs) # Теперь обрабатываем изображение с известными ID entity = self.get_entity() entity_type = self.get_entity_type() processed_paths = ImageProcessor.process_image( temp_image, entity_type, entity_id=entity.id, photo_id=self.id ) self.image = processed_paths['original'] # Сохраняем уровень качества self.quality_level = processed_paths.get('quality_level', 'acceptable') self.quality_warning = processed_paths.get('quality_warning', False) # Обновляем поля image, quality_level и quality_warning super().save(update_fields=['image', 'quality_level', 'quality_warning']) else: # Проверяем старый путь для удаления, если это обновление old_image_path = None if self.pk: try: old_obj = self.__class__.objects.get(pk=self.pk) if old_obj.image and old_obj.image != self.image: old_image_path = old_obj.image.name except self.__class__.DoesNotExist: pass # Проверяем, нужно ли обрабатывать изображение if self.image and old_image_path: # Обновление существующего изображения entity = self.get_entity() entity_type = self.get_entity_type() processed_paths = ImageProcessor.process_image( self.image, entity_type, entity_id=entity.id, photo_id=self.id ) self.image = processed_paths['original'] # Обновляем уровень качества self.quality_level = processed_paths.get('quality_level', 'acceptable') self.quality_warning = processed_paths.get('quality_warning', False) # Удаляем старые версии ImageProcessor.delete_all_versions( entity_type, old_image_path, entity_id=entity.id, photo_id=self.id ) # Обновляем поля image, quality_level и quality_warning super().save(update_fields=['image', 'quality_level', 'quality_warning']) else: # Просто сохраняем без обработки изображения super().save(*args, **kwargs) def delete(self, *args, **kwargs): """Удаляет все версии изображения при удалении фото""" import logging from ..utils.image_processor import ImageProcessor logger = logging.getLogger(__name__) if self.image: try: entity = self.get_entity() entity_type = self.get_entity_type() logger.info(f"[{self.__class__.__name__}.delete] Удаляем изображение: {self.image.name}") ImageProcessor.delete_all_versions( entity_type, self.image.name, entity_id=entity.id, photo_id=self.id ) logger.info(f"[{self.__class__.__name__}.delete] ✓ Все версии изображения удалены") except Exception as e: logger.error( f"[{self.__class__.__name__}.delete] ✗ Ошибка при удалении версий: {str(e)}", exc_info=True ) super().delete(*args, **kwargs) def get_thumbnail_url(self): """Получить URL миниатюры (150x150)""" from ..utils.image_service import ImageService return ImageService.get_thumbnail_url(self.image.name) def get_medium_url(self): """Получить URL среднего размера (400x400)""" from ..utils.image_service import ImageService return ImageService.get_medium_url(self.image.name) def get_large_url(self): """Получить URL большого размера (800x800)""" from ..utils.image_service import ImageService return ImageService.get_large_url(self.image.name) def get_original_url(self): """Получить URL оригинального изображения""" from ..utils.image_service import ImageService return ImageService.get_original_url(self.image.name) class ProductPhoto(BasePhoto): """ Модель для хранения фото товара (один товар может иметь несколько фото). Автоматически создает несколько размеров при загрузке: original, thumbnail, medium, large. Каждое фото автоматически оценивается по качеству на основе размера: - quality_level: Уровень качества (excellent/good/acceptable/poor/very_poor) - quality_warning: True если требует обновления перед выгрузкой на сайт """ QUALITY_LEVEL_CHOICES = [ ('excellent', 'Отлично (>= 2052px)'), ('good', 'Хорошо (1512-2051px)'), ('acceptable', 'Приемлемо (864-1511px)'), ('poor', 'Плохо (432-863px)'), ('very_poor', 'Очень плохо (< 432px)'), ] product = models.ForeignKey( 'Product', on_delete=models.CASCADE, related_name='photos', verbose_name="Товар" ) image = models.ImageField(upload_to='products/temp/', verbose_name="Оригинальное фото") # Оценка качества quality_level = models.CharField( max_length=15, choices=QUALITY_LEVEL_CHOICES, default='acceptable', db_index=True, verbose_name="Уровень качества", help_text='Определяется автоматически на основе размера изображения' ) quality_warning = models.BooleanField( default=False, db_index=True, verbose_name="Требует обновления", help_text='True если нужно обновить фото перед выгрузкой на сайт (poor или very_poor)' ) class Meta: verbose_name = "Фото товара" verbose_name_plural = "Фото товаров" ordering = ['order', '-created_at'] indexes = [ models.Index(fields=['quality_level']), models.Index(fields=['quality_warning']), models.Index(fields=['quality_warning', 'product']), # Для поиска товаров требующих обновления фото ] def __str__(self): return f"Фото для {self.product.name} ({self.get_quality_level_display()})" def get_entity(self): """Возвращает связанный товар""" return self.product def get_entity_type(self): """Возвращает тип сущности для путей""" return 'products' class ProductKitPhoto(BasePhoto): """ Модель для хранения фото комплекта (один комплект может иметь несколько фото). Автоматически создает несколько размеров при загрузке: original, thumbnail, medium, large. Каждое фото автоматически оценивается по качеству на основе размера. """ QUALITY_LEVEL_CHOICES = [ ('excellent', 'Отлично (>= 2052px)'), ('good', 'Хорошо (1512-2051px)'), ('acceptable', 'Приемлемо (864-1511px)'), ('poor', 'Плохо (432-863px)'), ('very_poor', 'Очень плохо (< 432px)'), ] kit = models.ForeignKey( 'ProductKit', on_delete=models.CASCADE, related_name='photos', verbose_name="Комплект" ) image = models.ImageField(upload_to='kits/temp/', verbose_name="Оригинальное фото") # Оценка качества quality_level = models.CharField( max_length=15, choices=QUALITY_LEVEL_CHOICES, default='acceptable', db_index=True, verbose_name="Уровень качества", help_text='Определяется автоматически на основе размера изображения' ) quality_warning = models.BooleanField( default=False, db_index=True, verbose_name="Требует обновления", help_text='True если нужно обновить фото перед выгрузкой на сайт' ) class Meta: verbose_name = "Фото комплекта" verbose_name_plural = "Фото комплектов" ordering = ['order', '-created_at'] indexes = [ models.Index(fields=['quality_level']), models.Index(fields=['quality_warning']), models.Index(fields=['quality_warning', 'kit']), ] def __str__(self): return f"Фото для {self.kit.name} ({self.get_quality_level_display()})" def get_entity(self): """Возвращает связанный комплект""" return self.kit def get_entity_type(self): """Возвращает тип сущности для путей""" return 'kits' class ProductCategoryPhoto(BasePhoto): """ Модель для хранения фото категории (одна категория может иметь несколько фото). Автоматически создает несколько размеров при загрузке: original, thumbnail, medium, large. Каждое фото автоматически оценивается по качеству на основе размера. """ QUALITY_LEVEL_CHOICES = [ ('excellent', 'Отлично (>= 2052px)'), ('good', 'Хорошо (1512-2051px)'), ('acceptable', 'Приемлемо (864-1511px)'), ('poor', 'Плохо (432-863px)'), ('very_poor', 'Очень плохо (< 432px)'), ] category = models.ForeignKey( 'ProductCategory', on_delete=models.CASCADE, related_name='photos', verbose_name="Категория" ) image = models.ImageField(upload_to='categories/temp/', verbose_name="Оригинальное фото") # Оценка качества quality_level = models.CharField( max_length=15, choices=QUALITY_LEVEL_CHOICES, default='acceptable', db_index=True, verbose_name="Уровень качества", help_text='Определяется автоматически на основе размера изображения' ) quality_warning = models.BooleanField( default=False, db_index=True, verbose_name="Требует обновления", help_text='True если нужно обновить фото перед выгрузкой на сайт' ) class Meta: verbose_name = "Фото категории" verbose_name_plural = "Фото категорий" ordering = ['order', '-created_at'] indexes = [ models.Index(fields=['quality_level']), models.Index(fields=['quality_warning']), models.Index(fields=['quality_warning', 'category']), ] def __str__(self): return f"Фото для {self.category.name} ({self.get_quality_level_display()})" def get_entity(self): """Возвращает связанную категорию""" return self.category def get_entity_type(self): """Возвращает тип сущности для путей""" return 'categories'