Files
octopus/myproject/products/views/photo_management.py
Andrey Smakotin ffc3b0c42d feat: implement role-based permissions for product views
- 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>
2025-12-01 22:44:36 +03:00

383 lines
14 KiB
Python
Raw 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):
"""
Универсальная установка фото как главного (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)