""" Универсальные функции для управления фотографиями товаров, комплектов и категорий. Устраняет дублирование кода для операций: delete, set_main, move_up, move_down. """ import json from django.shortcuts import get_object_or_404, redirect from django.contrib import messages from django.http import JsonResponse from django.views.decorators.http import require_http_methods from django.contrib.auth.decorators import login_required from ..models import ProductPhoto, ProductKitPhoto, ProductCategoryPhoto # ==================================== # Универсальные функции # ==================================== def generic_photo_delete(request, pk, photo_model, redirect_url_name, parent_attr, permission): """ Универсальное удаление фотографии. Args: request: HTTP request pk: ID фотографии photo_model: Модель фото (ProductPhoto, ProductKitPhoto, ProductCategoryPhoto) redirect_url_name: Имя URL для редиректа ('products:product-update', etc.) parent_attr: Имя атрибута родителя ('product', 'kit', 'category') permission: Требуемое разрешение ('products.change_product', etc.) """ photo = get_object_or_404(photo_model, pk=pk) parent = getattr(photo, parent_attr) parent_id = parent.id # Проверка прав доступа if not request.user.has_perm(permission): messages.error(request, 'У вас нет прав для удаления фотографий.') return redirect(redirect_url_name, pk=parent_id) photo.delete() messages.success(request, 'Фото успешно удалено!') return redirect(redirect_url_name, pk=parent_id) def generic_photo_set_main(request, pk, photo_model, redirect_url_name, parent_attr, permission): """ Универсальная установка фото как главного (is_main=True). Автоматически сбрасывает is_main=False у старого главного фото. Constraint на уровне БД гарантирует, что у сущности может быть только одно is_main=True. Args: request: HTTP request pk: ID фотографии photo_model: Модель фото redirect_url_name: Имя URL для редиректа parent_attr: Имя атрибута родителя ('product', 'kit', 'category') permission: Требуемое разрешение """ photo = get_object_or_404(photo_model, pk=pk) parent = getattr(photo, parent_attr) parent_id = parent.id # Проверка прав доступа if not request.user.has_perm(permission): messages.error(request, 'У вас нет прав для изменения порядка фотографий.') return redirect(redirect_url_name, pk=parent_id) # Если это уже главное фото, ничего не делаем if photo.is_main: messages.info(request, 'Это фото уже установлено как главное.') return redirect(redirect_url_name, pk=parent_id) # Сбрасываем is_main у старого главного фото filter_kwargs = {f"{parent_attr}_id": parent_id, 'is_main': True} old_main = photo_model.objects.filter(**filter_kwargs).first() if old_main: old_main.is_main = False old_main.save(update_fields=['is_main']) # Устанавливаем новое главное фото photo.is_main = True photo.save(update_fields=['is_main']) messages.success(request, 'Фото установлено как главное!') return redirect(redirect_url_name, pk=parent_id) def generic_photo_move_up(request, pk, photo_model, redirect_url_name, parent_attr, permission): """ Универсальное перемещение фото вверх (уменьшить order). Args: request: HTTP request pk: ID фотографии photo_model: Модель фото redirect_url_name: Имя URL для редиректа parent_attr: Имя атрибута родителя ('product', 'kit', 'category') permission: Требуемое разрешение """ photo = get_object_or_404(photo_model, pk=pk) parent = getattr(photo, parent_attr) parent_id = parent.id # Проверка прав доступа if not request.user.has_perm(permission): messages.error(request, 'У вас нет прав для изменения порядка фотографий.') return redirect(redirect_url_name, pk=parent_id) # Если это уже первое фото if photo.order == 0: messages.info(request, 'Это фото уже первое в списке.') return redirect(redirect_url_name, pk=parent_id) # Находим предыдущее фото filter_kwargs = { f"{parent_attr}_id": parent_id, 'order__lt': photo.order } prev_photo = photo_model.objects.filter(**filter_kwargs).order_by('-order').first() if prev_photo: # Меняем местами photo.order, prev_photo.order = prev_photo.order, photo.order photo.save() prev_photo.save() messages.success(request, 'Фото перемещено вверх!') return redirect(redirect_url_name, pk=parent_id) def generic_photo_move_down(request, pk, photo_model, redirect_url_name, parent_attr, permission): """ Универсальное перемещение фото вниз (увеличить order). Args: request: HTTP request pk: ID фотографии photo_model: Модель фото redirect_url_name: Имя URL для редиректа parent_attr: Имя атрибута родителя ('product', 'kit', 'category') permission: Требуемое разрешение """ photo = get_object_or_404(photo_model, pk=pk) parent = getattr(photo, parent_attr) parent_id = parent.id # Проверка прав доступа if not request.user.has_perm(permission): messages.error(request, 'У вас нет прав для изменения порядка фотографий.') return redirect(redirect_url_name, pk=parent_id) # Находим следующее фото filter_kwargs = { f"{parent_attr}_id": parent_id, 'order__gt': photo.order } next_photo = photo_model.objects.filter(**filter_kwargs).order_by('order').first() if next_photo: # Меняем местами photo.order, next_photo.order = next_photo.order, photo.order photo.save() next_photo.save() messages.success(request, 'Фото перемещено вниз!') else: messages.info(request, 'Это фото уже последнее в списке.') return redirect(redirect_url_name, pk=parent_id) # ==================================== # Обертки для Product Photos # ==================================== def product_photo_delete(request, pk): """Удаление фотографии товара""" return generic_photo_delete( request, pk, photo_model=ProductPhoto, redirect_url_name='products:product-update', parent_attr='product', permission='products.change_product' ) def product_photo_set_main(request, pk): """Установка фото товара как главного (order = 0)""" return generic_photo_set_main( request, pk, photo_model=ProductPhoto, redirect_url_name='products:product-update', parent_attr='product', permission='products.change_product' ) def product_photo_move_up(request, pk): """Переместить фото товара вверх (уменьшить order)""" return generic_photo_move_up( request, pk, photo_model=ProductPhoto, redirect_url_name='products:product-update', parent_attr='product', permission='products.change_product' ) def product_photo_move_down(request, pk): """Переместить фото товара вниз (увеличить order)""" return generic_photo_move_down( request, pk, photo_model=ProductPhoto, redirect_url_name='products:product-update', parent_attr='product', permission='products.change_product' ) # ==================================== # Обертки для ProductKit Photos # ==================================== def productkit_photo_delete(request, pk): """Удаление фотографии комплекта""" return generic_photo_delete( request, pk, photo_model=ProductKitPhoto, redirect_url_name='products:productkit-update', parent_attr='kit', permission='products.change_productkit' ) def productkit_photo_set_main(request, pk): """Установка фото комплекта как главного (order = 0)""" return generic_photo_set_main( request, pk, photo_model=ProductKitPhoto, redirect_url_name='products:productkit-update', parent_attr='kit', permission='products.change_productkit' ) def productkit_photo_move_up(request, pk): """Переместить фото комплекта вверх (уменьшить order)""" return generic_photo_move_up( request, pk, photo_model=ProductKitPhoto, redirect_url_name='products:productkit-update', parent_attr='kit', permission='products.change_productkit' ) def productkit_photo_move_down(request, pk): """Переместить фото комплекта вниз (увеличить order)""" return generic_photo_move_down( request, pk, photo_model=ProductKitPhoto, redirect_url_name='products:productkit-update', parent_attr='kit', permission='products.change_productkit' ) # ==================================== # Обертки для Category Photos # ==================================== def category_photo_delete(request, pk): """Удаление фотографии категории""" return generic_photo_delete( request, pk, photo_model=ProductCategoryPhoto, redirect_url_name='products:category-update', parent_attr='category', permission='products.change_productcategory' ) def category_photo_set_main(request, pk): """Установка фото категории как главного (order = 0)""" return generic_photo_set_main( request, pk, photo_model=ProductCategoryPhoto, redirect_url_name='products:category-update', parent_attr='category', permission='products.change_productcategory' ) def category_photo_move_up(request, pk): """Переместить фото категории вверх (уменьшить order)""" return generic_photo_move_up( request, pk, photo_model=ProductCategoryPhoto, redirect_url_name='products:category-update', parent_attr='category', permission='products.change_productcategory' ) def category_photo_move_down(request, pk): """Переместить фото категории вниз (увеличить order)""" return generic_photo_move_down( request, pk, photo_model=ProductCategoryPhoto, redirect_url_name='products:category-update', parent_attr='category', permission='products.change_productcategory' ) # ==================================== # AJAX Endpoints для массового удаления # ==================================== @require_http_methods(["POST"]) @login_required def product_photos_delete_bulk(request): """ AJAX endpoint для массового удаления фотографий товара. Ожидает JSON: {photo_ids: [1, 2, 3]} Возвращает JSON: {success: true, deleted: 3} или {success: false, error: "..."} """ # Проверка прав доступа if not request.user.has_perm('products.change_product'): return JsonResponse({ 'success': False, 'error': 'У вас нет прав для удаления фотографий' }, status=403) try: # Получаем список photo_ids из JSON тела запроса data = json.loads(request.body) photo_ids = data.get('photo_ids', []) if not photo_ids or not isinstance(photo_ids, list): return JsonResponse({ 'success': False, 'error': 'Неверный формат: требуется список photo_ids' }, status=400) # Удаляем фотографии deleted_count = 0 for photo_id in photo_ids: try: photo = ProductPhoto.objects.get(pk=photo_id) photo.delete() # Это вызовет ImageProcessor.delete_all_versions() deleted_count += 1 except ProductPhoto.DoesNotExist: # Если фото не найдена, просто пропускаем continue except Exception as e: # Логируем ошибку но продолжаем удаление остальных import logging logger = logging.getLogger(__name__) logger.error(f"Error deleting photo {photo_id}: {str(e)}", exc_info=True) continue return JsonResponse({ 'success': True, 'deleted': deleted_count }) except json.JSONDecodeError: return JsonResponse({ 'success': False, 'error': 'Неверный JSON формат' }, status=400) except Exception as e: import logging logger = logging.getLogger(__name__) logger.error(f"Bulk photo deletion error: {str(e)}", exc_info=True) return JsonResponse({ 'success': False, 'error': f'Ошибка сервера: {str(e)}' }, status=500)