Улучшение системы работы с фото: добавлена команда очистки битых записей и оптимизация обработки изображений
This commit is contained in:
179
myproject/products/management/commands/cleanup_missing_photos.py
Normal file
179
myproject/products/management/commands/cleanup_missing_photos.py
Normal file
@@ -0,0 +1,179 @@
|
||||
"""
|
||||
Management команда для удаления записей фотографий, файлы которых не существуют на диске.
|
||||
Проверяет ProductPhoto, ProductKitPhoto, ProductCategoryPhoto.
|
||||
|
||||
Использование:
|
||||
# Для конкретного тенанта
|
||||
python manage.py cleanup_missing_photos --schema=anatol --dry-run
|
||||
python manage.py cleanup_missing_photos --schema=anatol
|
||||
|
||||
# Для всех тенантов
|
||||
python manage.py cleanup_missing_photos --all-tenants --dry-run
|
||||
python manage.py cleanup_missing_photos --all-tenants
|
||||
"""
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.core.files.storage import default_storage
|
||||
from django_tenants.utils import schema_context, get_tenant_model
|
||||
from products.models import ProductPhoto, ProductKitPhoto, ProductCategoryPhoto
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Удаляет записи фотографий, файлы которых не существуют на диске'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
'--dry-run',
|
||||
action='store_true',
|
||||
help='Показать что будет удалено, но не удалять',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--schema',
|
||||
type=str,
|
||||
help='Schema name (subdomain) тенанта для обработки',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--all-tenants',
|
||||
action='store_true',
|
||||
help='Обработать все тенанты (кроме public)',
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
dry_run = options['dry_run']
|
||||
schema_name = options.get('schema')
|
||||
all_tenants = options.get('all_tenants')
|
||||
|
||||
if not schema_name and not all_tenants:
|
||||
self.stdout.write(
|
||||
self.style.ERROR(
|
||||
'\n❌ Ошибка: укажите либо --schema=<имя>, либо --all-tenants\n'
|
||||
'Примеры:\n'
|
||||
' python manage.py cleanup_missing_photos --schema=anatol --dry-run\n'
|
||||
' python manage.py cleanup_missing_photos --all-tenants\n'
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
# Получаем список тенантов для обработки
|
||||
Tenant = get_tenant_model()
|
||||
|
||||
if all_tenants:
|
||||
tenants = Tenant.objects.exclude(schema_name='public')
|
||||
else:
|
||||
try:
|
||||
tenants = [Tenant.objects.get(schema_name=schema_name)]
|
||||
except Tenant.DoesNotExist:
|
||||
self.stdout.write(
|
||||
self.style.ERROR(f'\n❌ Тенант с schema "{schema_name}" не найден\n')
|
||||
)
|
||||
return
|
||||
|
||||
# Обрабатываем каждый тенант
|
||||
for tenant in tenants:
|
||||
self._process_tenant(tenant, dry_run)
|
||||
|
||||
def _process_tenant(self, tenant, dry_run):
|
||||
"""Обработка одного тенанта"""
|
||||
self.stdout.write('\n' + '=' * 70)
|
||||
self.stdout.write(
|
||||
self.style.SUCCESS(f'Тенант: {tenant.name} (schema: {tenant.schema_name})')
|
||||
)
|
||||
self.stdout.write('=' * 70)
|
||||
|
||||
if dry_run:
|
||||
self.stdout.write(self.style.WARNING('РЕЖИМ ПРОВЕРКИ (записи не будут удалены)\n'))
|
||||
else:
|
||||
self.stdout.write(self.style.WARNING('УДАЛЕНИЕ БИТЫХ ФОТОГРАФИЙ\n'))
|
||||
|
||||
with schema_context(tenant.schema_name):
|
||||
# Счетчики
|
||||
total_checked = 0
|
||||
total_missing = 0
|
||||
total_deleted = 0
|
||||
|
||||
# Проверяем ProductPhoto
|
||||
self.stdout.write('\n1. Проверка фотографий товаров (ProductPhoto)...')
|
||||
product_photos = ProductPhoto.objects.select_related('product').all()
|
||||
|
||||
for photo in product_photos:
|
||||
total_checked += 1
|
||||
|
||||
# Проверяем существование файла
|
||||
if not photo.image or not default_storage.exists(photo.image.name):
|
||||
total_missing += 1
|
||||
product_name = photo.product.name if photo.product else 'N/A'
|
||||
file_path = photo.image.name if photo.image else 'N/A'
|
||||
|
||||
self.stdout.write(
|
||||
self.style.ERROR(
|
||||
f' ❌ Товар: {product_name} (ID: {photo.product_id}) - '
|
||||
f'Файл не найден: {file_path}'
|
||||
)
|
||||
)
|
||||
|
||||
if not dry_run:
|
||||
photo.delete()
|
||||
total_deleted += 1
|
||||
self.stdout.write(self.style.SUCCESS(f' ✓ Запись удалена'))
|
||||
|
||||
# Проверяем ProductKitPhoto
|
||||
self.stdout.write('\n2. Проверка фотографий комплектов (ProductKitPhoto)...')
|
||||
kit_photos = ProductKitPhoto.objects.select_related('kit').all()
|
||||
|
||||
for photo in kit_photos:
|
||||
total_checked += 1
|
||||
|
||||
if not photo.image or not default_storage.exists(photo.image.name):
|
||||
total_missing += 1
|
||||
kit_name = photo.kit.name if photo.kit else 'N/A'
|
||||
file_path = photo.image.name if photo.image else 'N/A'
|
||||
|
||||
self.stdout.write(
|
||||
self.style.ERROR(
|
||||
f' ❌ Комплект: {kit_name} (ID: {photo.kit_id}) - '
|
||||
f'Файл не найден: {file_path}'
|
||||
)
|
||||
)
|
||||
|
||||
if not dry_run:
|
||||
photo.delete()
|
||||
total_deleted += 1
|
||||
self.stdout.write(self.style.SUCCESS(f' ✓ Запись удалена'))
|
||||
|
||||
# Проверяем ProductCategoryPhoto
|
||||
self.stdout.write('\n3. Проверка фотографий категорий (ProductCategoryPhoto)...')
|
||||
category_photos = ProductCategoryPhoto.objects.select_related('category').all()
|
||||
|
||||
for photo in category_photos:
|
||||
total_checked += 1
|
||||
|
||||
if not photo.image or not default_storage.exists(photo.image.name):
|
||||
total_missing += 1
|
||||
category_name = photo.category.name if photo.category else 'N/A'
|
||||
file_path = photo.image.name if photo.image else 'N/A'
|
||||
|
||||
self.stdout.write(
|
||||
self.style.ERROR(
|
||||
f' ❌ Категория: {category_name} (ID: {photo.category_id}) - '
|
||||
f'Файл не найден: {file_path}'
|
||||
)
|
||||
)
|
||||
|
||||
if not dry_run:
|
||||
photo.delete()
|
||||
total_deleted += 1
|
||||
self.stdout.write(self.style.SUCCESS(f' ✓ Запись удалена'))
|
||||
|
||||
# Итоговая статистика для тенанта
|
||||
self.stdout.write('\n' + '-' * 70)
|
||||
self.stdout.write(self.style.SUCCESS(f'Всего проверено записей: {total_checked}'))
|
||||
self.stdout.write(self.style.WARNING(f'Найдено битых записей: {total_missing}'))
|
||||
|
||||
if dry_run:
|
||||
self.stdout.write(
|
||||
self.style.WARNING(
|
||||
f'\n⚠ РЕЖИМ ПРОВЕРКИ: записи НЕ удалены'
|
||||
)
|
||||
)
|
||||
else:
|
||||
self.stdout.write(self.style.SUCCESS(f'Удалено записей: {total_deleted}'))
|
||||
self.stdout.write(self.style.SUCCESS('\n✓ Очистка завершена'))
|
||||
Reference in New Issue
Block a user