- Add view mixins (RoleRequiredMixin, OwnerRequiredMixin, ManagerOwnerRequiredMixin) to user_roles/mixins.py - Replace PermissionRequiredMixin with ManagerOwnerRequiredMixin in all product views - Remove permission_required attributes from view classes - Owner and Manager roles now grant access without Django model permissions This allows owners to access all product functionality through their custom role, without needing to be superusers or have explicit Django permissions. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
383 lines
14 KiB
Python
383 lines
14 KiB
Python
"""
|
||
Универсальные функции для управления фотографиями товаров, комплектов и категорий.
|
||
Устраняет дублирование кода для операций: 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):
|
||
"""
|
||
Универсальная установка фото как главного (order = 0).
|
||
|
||
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}
|
||
photos = photo_model.objects.filter(**filter_kwargs).order_by('order')
|
||
|
||
# Если это уже главное фото, ничего не делаем
|
||
if photo.order == 0:
|
||
messages.info(request, 'Это фото уже установлено как главное.')
|
||
return redirect(redirect_url_name, pk=parent_id)
|
||
|
||
# Меняем порядок: текущее главное фото становится вторым
|
||
old_order = photo.order
|
||
for p in photos:
|
||
if p.pk == photo.pk:
|
||
p.order = 0
|
||
p.save()
|
||
elif p.order == 0:
|
||
p.order = old_order
|
||
p.save()
|
||
|
||
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)
|