Files
octopus/myproject/products/views/photo_management.py
Andrey Smakotin 036b9d1634 feat(products): добавить загрузку изображений по URL для комплектов
Добавлена возможность загружать фотографии комплекта по прямой ссылке
на формах создания и редактирования. JavaScript скачивает изображение
и добавляет его как файл в форму для отправки на сервер.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-21 23:24:42 +03:00

447 lines
17 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Универсальные функции для управления фотографиями товаров, комплектов и категорий.
Устраняет дублирование кода для операций: 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)
@require_http_methods(["POST"])
@login_required
def productkit_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_productkit'):
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 = ProductKitPhoto.objects.get(pk=photo_id)
photo.delete() # Это вызовет ImageProcessor.delete_all_versions()
deleted_count += 1
except ProductKitPhoto.DoesNotExist:
# Если фото не найдена, просто пропускаем
continue
except Exception as e:
# Логируем ошибку но продолжаем удаление остальных
import logging
logger = logging.getLogger(__name__)
logger.error(f"Error deleting kit 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 kit photo deletion error: {str(e)}", exc_info=True)
return JsonResponse({
'success': False,
'error': f'Ошибка сервера: {str(e)}'
}, status=500)