feat: Реализовать систему оценки качества фотографий товаров (Фаза 1)
Добавлена полностью гибкая система для оценки качества фотографий на основе размеров:
Конфигурация (settings.py):
- IMAGE_QUALITY_LEVELS: Пороги качества как доля от максимума (95%, 70%, 40%, 20%)
- IMAGE_QUALITY_LABELS: Описания, цвета и рекомендации для каждого уровня
- Система полностью адаптивна - меняется max_width в settings → пороги пересчитываются
Валидатор (validators/image_validators.py):
- get_max_dimension_from_config() - динамически читает из IMAGE_PROCESSING_CONFIG
- get_image_quality_level() - определяет уровень качества (excellent/good/acceptable/poor/very_poor)
- get_quality_info() - информация о уровне из IMAGE_QUALITY_LABELS
- validate_product_image() - комплексная валидация для UI
Модели (models/photos.py):
- ProductPhoto, ProductKitPhoto, ProductCategoryPhoto дополнены полями:
- quality_level: уровень качества (CharField с choices)
- quality_warning: требует ли обновления (BooleanField)
- Добавлены индексы для быстрого поиска товаров требующих обновления фото
Обработчик (image_processor.py):
- process_image() теперь возвращает дополнительно:
- width, height: размеры оригинального изображения
- quality_level: уровень качества
- quality_warning: нужно ли обновить перед выгрузкой
- Вызывает валидатор автоматически при обработке
Логика сохранения (photos.py -> save()):
- При сохранении нового фото автоматически вычисляет quality_level и quality_warning
- При обновлении существующего фото пересчитывает качество
- Сохраняет все три поля атомарно
Система полностью готова к:
- Phase 2: Admin интерфейс с фильтрами и индикаторами
- Phase 3: Фронтенд с визуальными индикаторами в формах и API
Примеры использования:
>>> from products.validators.image_validators import get_image_quality_level
>>> get_image_quality_level(1400, 1400) # если max=2160
('good', False) # 1400/2160 = 64.8% >= 70%? Нет, но >= 40%
>>> get_image_quality_level(400, 400)
('poor', True) # Требует обновления
🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
235
myproject/products/validators/image_validators.py
Normal file
235
myproject/products/validators/image_validators.py
Normal file
@@ -0,0 +1,235 @@
|
||||
"""
|
||||
Валидаторы и утилиты для оценки качества изображений товаров.
|
||||
|
||||
Система полностью гибкая - все параметры читаются из settings:
|
||||
- Пороги качества из IMAGE_QUALITY_LEVELS
|
||||
- Описания из IMAGE_QUALITY_LABELS
|
||||
- Максимальные размеры из IMAGE_PROCESSING_CONFIG
|
||||
|
||||
Это позволяет менять поведение без изменения кода.
|
||||
"""
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from PIL import Image
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_max_dimension_from_config():
|
||||
"""
|
||||
Динамически читает максимальный размер из IMAGE_PROCESSING_CONFIG.
|
||||
|
||||
Получает max_width и max_height из original формата и возвращает максимальное значение.
|
||||
|
||||
Returns:
|
||||
int: Максимальный размер оригинального изображения (из settings)
|
||||
|
||||
Examples:
|
||||
>>> get_max_dimension_from_config()
|
||||
2160 # если original.max_width=2160 и original.max_height=2160
|
||||
"""
|
||||
config = settings.IMAGE_PROCESSING_CONFIG
|
||||
original_config = config.get('formats', {}).get('original', {})
|
||||
|
||||
# Получаем параметры с дефолтными значениями
|
||||
max_width = original_config.get('max_width', 2160)
|
||||
max_height = original_config.get('max_height', 2160)
|
||||
|
||||
# Возвращаем максимальное значение
|
||||
max_dimension = max(max_width, max_height)
|
||||
|
||||
logger.debug(f"Max dimension from config: {max_dimension}px")
|
||||
return max_dimension
|
||||
|
||||
|
||||
def get_image_quality_level(width, height):
|
||||
"""
|
||||
Определяет уровень качества изображения на основе его размеров.
|
||||
|
||||
Логика:
|
||||
1. Берет минимальное измерение (ширина или высота)
|
||||
2. Вычисляет его как процент от максимального размера (из settings)
|
||||
3. Сравнивает с пороги из IMAGE_QUALITY_LEVELS
|
||||
4. Возвращает уровень качества и флаг требует ли обновления
|
||||
|
||||
Args:
|
||||
width (int): Ширина изображения в пиксели
|
||||
height (int): Высота изображения в пиксели
|
||||
|
||||
Returns:
|
||||
tuple: (quality_level, needs_update)
|
||||
quality_level (str): Один из: 'excellent', 'good', 'acceptable', 'poor', 'very_poor'
|
||||
needs_update (bool): True если требует обновления перед выгрузкой
|
||||
|
||||
Examples:
|
||||
>>> get_image_quality_level(2160, 2160)
|
||||
('excellent', False)
|
||||
|
||||
>>> get_image_quality_level(1400, 1400) # если max=2160
|
||||
('good', False) # 1400/2160 = 0.648 >= 0.70? НЕТ, но >= 0.40 (acceptable)
|
||||
|
||||
>>> get_image_quality_level(800, 800) # если max=2160
|
||||
('acceptable', False) # 800/2160 = 0.370 >= 0.40? НЕТ, но >= 0.20 (poor)
|
||||
"""
|
||||
min_dimension = min(width, height)
|
||||
max_dimension = get_max_dimension_from_config()
|
||||
|
||||
# Читаем пороги из settings с fallback значениями
|
||||
quality_levels = getattr(settings, 'IMAGE_QUALITY_LEVELS', {
|
||||
'excellent': 0.95,
|
||||
'good': 0.70,
|
||||
'acceptable': 0.40,
|
||||
'poor': 0.20,
|
||||
})
|
||||
|
||||
# Вычисляем процент от максимума
|
||||
quality_percent = min_dimension / max_dimension
|
||||
|
||||
logger.debug(f"Image size: {width}x{height}px, Quality: {quality_percent:.1%} of max {max_dimension}px")
|
||||
|
||||
# Определяем уровень качества
|
||||
# 'poor' и 'very_poor' требуют обновления перед выгрузкой на сайт
|
||||
if quality_percent >= quality_levels.get('excellent', 0.95):
|
||||
return 'excellent', False
|
||||
elif quality_percent >= quality_levels.get('good', 0.70):
|
||||
return 'good', False
|
||||
elif quality_percent >= quality_levels.get('acceptable', 0.40):
|
||||
return 'acceptable', False
|
||||
elif quality_percent >= quality_levels.get('poor', 0.20):
|
||||
return 'poor', True # ← Требует обновления
|
||||
else:
|
||||
return 'very_poor', True # ← Требует обновления
|
||||
|
||||
|
||||
def get_quality_info(quality_level):
|
||||
"""
|
||||
Получить полную информацию о уровне качества.
|
||||
|
||||
Читает описание, цвет, иконку и рекомендацию из IMAGE_QUALITY_LABELS в settings.
|
||||
|
||||
Args:
|
||||
quality_level (str): Уровень качества ('excellent', 'good', 'acceptable', 'poor', 'very_poor')
|
||||
|
||||
Returns:
|
||||
dict: Информация о уровне с ключами:
|
||||
- label: Название уровня
|
||||
- short_label: Краткое название с иконкой
|
||||
- description: Описание
|
||||
- color: Цвет для UI (success, info, warning, danger)
|
||||
- icon: Иконка (✓, ◐, ⚠, ✗)
|
||||
- recommendation: Рекомендация для пользователя
|
||||
- badge_class: CSS класс для бутстрапа
|
||||
|
||||
Examples:
|
||||
>>> get_quality_info('excellent')
|
||||
{'label': 'Отлично', 'color': 'success', ...}
|
||||
|
||||
>>> get_quality_info('poor')
|
||||
{'label': 'Плохо', 'color': 'danger', ...}
|
||||
"""
|
||||
labels = getattr(settings, 'IMAGE_QUALITY_LABELS', {})
|
||||
default_info = {
|
||||
'label': 'Неизвестно',
|
||||
'short_label': 'Неизвестно',
|
||||
'description': 'Не удалось определить качество',
|
||||
'color': 'secondary',
|
||||
'icon': '?',
|
||||
'recommendation': 'Обновите изображение',
|
||||
'badge_class': 'badge-secondary',
|
||||
}
|
||||
return labels.get(quality_level, default_info)
|
||||
|
||||
|
||||
def validate_product_image(image_file):
|
||||
"""
|
||||
Валидация изображения товара.
|
||||
|
||||
Проверяет что файл является валидным изображением и может быть открыт.
|
||||
Не отклоняет маленькие изображения - просто определяет их качество.
|
||||
|
||||
Args:
|
||||
image_file: Django UploadedFile объект
|
||||
|
||||
Returns:
|
||||
dict: Результат валидации с ключами:
|
||||
- valid (bool): Прошло ли валидацию
|
||||
- width (int): Ширина изображения
|
||||
- height (int): Высота изображения
|
||||
- quality_level (str): Уровень качества
|
||||
- needs_update (bool): Требует ли обновления
|
||||
- message (str): Сообщение для пользователя
|
||||
- error (str): Сообщение об ошибке (если valid=False)
|
||||
|
||||
Raises:
|
||||
ValidationError: Если файл не является изображением
|
||||
"""
|
||||
try:
|
||||
# Открываем изображение для получения размеров
|
||||
img = Image.open(image_file)
|
||||
width, height = img.size
|
||||
|
||||
# Определяем качество
|
||||
quality_level, needs_update = get_image_quality_level(width, height)
|
||||
quality_info = get_quality_info(quality_level)
|
||||
|
||||
# Подготавливаем сообщение для пользователя
|
||||
message = (
|
||||
f"Размер изображения: {width}×{height}px. "
|
||||
f"Качество: {quality_info['label']} - {quality_info['description']}."
|
||||
)
|
||||
|
||||
if needs_update:
|
||||
message += f" {quality_info['recommendation']}"
|
||||
|
||||
logger.info(f"Image validation: {width}x{height}px → {quality_level}")
|
||||
|
||||
return {
|
||||
'valid': True,
|
||||
'width': width,
|
||||
'height': height,
|
||||
'quality_level': quality_level,
|
||||
'needs_update': needs_update,
|
||||
'message': message,
|
||||
'error': None,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Не удалось обработать изображение: {str(e)}"
|
||||
logger.error(f"Image validation error: {str(e)}", exc_info=True)
|
||||
|
||||
return {
|
||||
'valid': False,
|
||||
'width': None,
|
||||
'height': None,
|
||||
'quality_level': None,
|
||||
'needs_update': False,
|
||||
'message': None,
|
||||
'error': error_msg,
|
||||
}
|
||||
|
||||
|
||||
def get_quality_level_percentage(quality_level):
|
||||
"""
|
||||
Получить процент от максимума для уровня качества.
|
||||
|
||||
Используется для визуализации прогресс-баров в UI.
|
||||
|
||||
Args:
|
||||
quality_level (str): Уровень качества
|
||||
|
||||
Returns:
|
||||
float: Процент от максимума (0.0 - 1.0)
|
||||
"""
|
||||
quality_levels = getattr(settings, 'IMAGE_QUALITY_LEVELS', {
|
||||
'excellent': 0.95,
|
||||
'good': 0.70,
|
||||
'acceptable': 0.40,
|
||||
'poor': 0.20,
|
||||
})
|
||||
|
||||
if quality_level == 'very_poor':
|
||||
return 0.1 # Визуально покажет как очень мало
|
||||
|
||||
return quality_levels.get(quality_level, 0.5)
|
||||
Reference in New Issue
Block a user