Files
octopus/myproject/products/views/productkit_views.py
Andrey Smakotin 9363527e50 Fix: Auto-cleanup temp files after photo processing
- Added temp file deletion in Celery task after successful processing
- Added temp file cleanup in sync fallback method
- Added temp file removal in delete() if processing never completed
- Prevents accumulation of orphaned files in media/<entity>/temp/ folders
2025-11-15 22:28:41 +03:00

534 lines
24 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.
"""
CRUD представления для комплектов товаров (ProductKit).
"""
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
from django.views.generic import ListView, CreateView, DetailView, UpdateView, DeleteView
from django.urls import reverse_lazy
from django.shortcuts import redirect
from django.db import transaction, IntegrityError
from ..models import ProductKit, ProductCategory, ProductTag, ProductKitPhoto, Product, ProductVariantGroup
from ..forms import ProductKitForm, KitItemFormSetCreate, KitItemFormSetUpdate
from .utils import handle_photos
class ProductKitListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
model = ProductKit
template_name = 'products/productkit_list.html'
context_object_name = 'kits'
permission_required = 'products.view_productkit'
paginate_by = 10
def get_queryset(self):
queryset = super().get_queryset()
queryset = queryset.prefetch_related('categories', 'photos', 'kit_items', 'tags')
# Скрываем временные комплекты из общего каталога
queryset = queryset.filter(is_temporary=False)
# Поиск по названию
search_query = self.request.GET.get('search')
if search_query:
queryset = queryset.filter(name__icontains=search_query)
# Фильтр по категории
category_id = self.request.GET.get('category')
if category_id:
queryset = queryset.filter(categories__id=category_id)
# Фильтр по статусу (новая система)
status_filter = self.request.GET.get('status')
if status_filter:
queryset = queryset.filter(status=status_filter)
else:
# Фильтр по is_active для обратной совместимости (старая система)
is_active = self.request.GET.get('is_active')
if is_active == '1':
queryset = queryset.filter(status='active')
elif is_active == '0':
queryset = queryset.filter(status__in=['archived', 'discontinued'])
return queryset.order_by('-created_at')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# Данные для фильтров
context['filters'] = {
'categories': ProductCategory.objects.filter(is_active=True),
'tags': ProductTag.objects.filter(is_active=True),
'current': {
'search': self.request.GET.get('search', ''),
'category': self.request.GET.get('category', ''),
'is_active': self.request.GET.get('is_active', ''),
'tags': self.request.GET.getlist('tags'),
}
}
# Кнопки действий
action_buttons = []
if self.request.user.has_perm('products.add_productkit'):
action_buttons.append({
'url': reverse_lazy('products:productkit-create'),
'text': 'Создать комплект',
'class': 'btn-primary',
'icon': 'plus-circle'
})
action_buttons.append({
'url': reverse_lazy('products:product-list'),
'text': 'К товарам',
'class': 'btn-outline-primary',
'icon': 'box'
})
context['action_buttons'] = action_buttons
return context
class ProductKitCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
"""
View для создания нового комплекта с добавлением компонентов на одной странице.
"""
model = ProductKit
form_class = ProductKitForm
template_name = 'products/productkit_create.html'
permission_required = 'products.add_productkit'
def post(self, request, *args, **kwargs):
"""
Обрабатываем POST данные и очищаем ID товаров/комплектов от префиксов.
API возвращает ID в формате "product_123" или "kit_456", но Django ожидает числа.
"""
# Создаем изменяемую копию POST данных
post_data = request.POST.copy()
# Очищаем product ID от префиксов (product_123 -> 123)
for key in post_data.keys():
if key.endswith('-product') and post_data[key]:
value = post_data[key]
if '_' in value:
# Извлекаем числовой ID из "product_123"
numeric_id = value.split('_')[1]
post_data[key] = numeric_id
# Заменяем request.POST на очищенные данные
request.POST = post_data
return super().post(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
if self.request.POST:
context['kititem_formset'] = KitItemFormSetCreate(self.request.POST, prefix='kititem')
# При ошибке валидации: извлекаем выбранные товары для предзагрузки в Select2
from ..models import Product, ProductVariantGroup
selected_products = {}
selected_variants = {}
for key, value in self.request.POST.items():
if '-product' in key and value:
try:
product = Product.objects.get(id=value)
text = product.name
if product.sku:
text += f" ({product.sku})"
# Получаем actual_price: приоритет sale_price > price
actual_price = product.sale_price if product.sale_price else product.price
selected_products[key] = {
'id': product.id,
'text': text,
'price': str(product.price) if product.price else None,
'actual_price': str(actual_price) if actual_price else '0'
}
except Product.DoesNotExist:
pass
if '-variant_group' in key and value:
try:
variant_group = ProductVariantGroup.objects.prefetch_related(
'items__product'
).get(id=value)
variant_price = variant_group.price or 0
count = variant_group.items.count()
selected_variants[key] = {
'id': variant_group.id,
'text': f"{variant_group.name} ({count} вариантов)",
'price': str(variant_price),
'actual_price': str(variant_price),
'type': 'variant',
'count': count
}
except ProductVariantGroup.DoesNotExist:
pass
context['selected_products'] = selected_products
context['selected_variants'] = selected_variants
else:
context['kititem_formset'] = KitItemFormSetCreate(prefix='kititem')
return context
def form_valid(self, form):
# Получаем формсет из POST с правильным префиксом
kititem_formset = KitItemFormSetCreate(self.request.POST, prefix='kititem')
# Проверяем валидность основной формы
if not form.is_valid():
messages.error(self.request, 'Пожалуйста, исправьте ошибки в основной форме комплекта.')
return self.form_invalid(form)
if not kititem_formset.is_valid():
# Если формсет невалиден, показываем форму с ошибками
messages.error(self.request, 'Пожалуйста, исправьте ошибки в компонентах комплекта.')
return self.form_invalid(form)
try:
with transaction.atomic():
# Сохраняем основную форму (комплект)
self.object = form.save(commit=True)
# Убеждаемся что объект в БД
if not self.object.pk:
raise Exception("Не удалось сохранить комплект в базу данных")
# Сохраняем компоненты
kititem_formset.instance = self.object
saved_items = kititem_formset.save()
# Пересчитываем базовую цену после сохранения всех компонентов
self.object.recalculate_base_price()
# Обработка фотографий
handle_photos(self.request, self.object, ProductKitPhoto, 'kit')
messages.success(
self.request,
f'Комплект "{self.object.name}" успешно создан!'
)
return redirect('products:productkit-list')
except IntegrityError as e:
# Обработка нарушения уникальности в БД
error_msg = str(e).lower()
if 'unique_active_kit_name' in error_msg or ('name' in error_msg and 'duplicate' in error_msg):
messages.error(
self.request,
f'Ошибка: комплект с названием "{form.instance.name}" уже существует. '
'Пожалуйста, используйте другое название.'
)
elif 'slug' in error_msg or 'unique' in error_msg:
messages.error(
self.request,
'Ошибка: комплект с таким названием уже существует. '
'Пожалуйста, используйте другое название.'
)
else:
messages.error(
self.request,
'Ошибка при сохранении комплекта. Пожалуйста, проверьте введённые данные.'
)
return self.form_invalid(form)
except Exception as e:
messages.error(self.request, f'Ошибка при сохранении: {str(e)}')
import traceback
traceback.print_exc()
return self.form_invalid(form)
def form_invalid(self, form):
# Получаем формсет для отображения ошибок
context = self.get_context_data(form=form)
return self.render_to_response(context)
class ProductKitUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
"""
View для редактирования существующего комплекта и добавления товаров.
"""
model = ProductKit
form_class = ProductKitForm
template_name = 'products/productkit_edit.html'
permission_required = 'products.change_productkit'
def post(self, request, *args, **kwargs):
"""
Обрабатываем POST данные и очищаем ID товаров/комплектов от префиксов.
API возвращает ID в формате "product_123" или "kit_456", но Django ожидает числа.
"""
# Создаем изменяемую копию POST данных
post_data = request.POST.copy()
# Очищаем product ID от префиксов (product_123 -> 123)
for key in post_data.keys():
if key.endswith('-product') and post_data[key]:
value = post_data[key]
if '_' in value:
# Извлекаем числовой ID из "product_123"
numeric_id = value.split('_')[1]
post_data[key] = numeric_id
# Заменяем request.POST на очищенные данные
request.POST = post_data
return super().post(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
if self.request.POST:
context['kititem_formset'] = KitItemFormSetUpdate(self.request.POST, instance=self.object, prefix='kititem')
# При ошибке валидации - подготавливаем данные для Select2
selected_products = {}
selected_variants = {}
for key, value in self.request.POST.items():
if '-product' in key and value:
try:
# Очищаем ID от префикса если есть
numeric_value = value.split('_')[1] if '_' in value else value
product = Product.objects.get(id=numeric_value)
text = product.name
if product.sku:
text += f" ({product.sku})"
actual_price = product.sale_price if product.sale_price else product.price
selected_products[key] = {
'id': product.id,
'text': text,
'price': str(product.price) if product.price else None,
'actual_price': str(actual_price) if actual_price else '0'
}
except Product.DoesNotExist:
pass
if '-variant_group' in key and value:
try:
variant_group = ProductVariantGroup.objects.prefetch_related(
'items__product'
).get(id=value)
variant_price = variant_group.price or 0
count = variant_group.items.count()
selected_variants[key] = {
'id': variant_group.id,
'text': f"{variant_group.name} ({count} вариантов)",
'price': str(variant_price),
'actual_price': str(variant_price),
'type': 'variant',
'count': count
}
except ProductVariantGroup.DoesNotExist:
pass
context['selected_products'] = selected_products
context['selected_variants'] = selected_variants
else:
context['kititem_formset'] = KitItemFormSetUpdate(instance=self.object, prefix='kititem')
# Подготавливаем данные для предзагрузки в Select2
selected_products = {}
selected_variants = {}
for item in self.object.kit_items.all():
form_prefix = f"kititem-{item.id}"
if item.product:
product = item.product
text = product.name
if product.sku:
text += f" ({product.sku})"
actual_price = product.sale_price if product.sale_price else product.price
selected_products[f"{form_prefix}-product"] = {
'id': product.id,
'text': text,
'price': str(product.price) if product.price else None,
'actual_price': str(actual_price) if actual_price else '0'
}
if item.variant_group:
variant_group = ProductVariantGroup.objects.prefetch_related(
'items__product'
).get(id=item.variant_group.id)
variant_price = variant_group.price or 0
count = variant_group.items.count()
selected_variants[f"{form_prefix}-variant_group"] = {
'id': variant_group.id,
'text': f"{variant_group.name} ({count} вариантов)",
'price': str(variant_price),
'actual_price': str(variant_price),
'type': 'variant',
'count': count
}
context['selected_products'] = selected_products
context['selected_variants'] = selected_variants
context['productkit_photos'] = self.object.photos.all().order_by('order', 'created_at')
context['photos_count'] = self.object.photos.count()
return context
def form_valid(self, form):
# Получаем формсет из POST с правильным префиксом
kititem_formset = KitItemFormSetUpdate(self.request.POST, instance=self.object, prefix='kititem')
# Проверяем валидность основной формы и формсета
if not form.is_valid():
messages.error(self.request, 'Пожалуйста, исправьте ошибки в основной форме комплекта.')
return self.form_invalid(form)
if not kititem_formset.is_valid():
# Если формсет невалиден, показываем форму с ошибками
messages.error(self.request, 'Пожалуйста, исправьте ошибки в компонентах комплекта.')
return self.form_invalid(form)
try:
with transaction.atomic():
# Сохраняем основную форму
self.object = form.save(commit=True)
# Сохраняем компоненты
kititem_formset.instance = self.object
kititem_formset.save()
# Пересчитываем базовую цену после сохранения всех компонентов
self.object.recalculate_base_price()
# Обработка фотографий
handle_photos(self.request, self.object, ProductKitPhoto, 'kit')
messages.success(self.request, f'Комплект "{self.object.name}" успешно обновлен!')
# Проверяем, какую кнопку нажали
if self.request.POST.get('action') == 'continue':
return redirect('products:productkit-update', pk=self.object.pk)
else:
return redirect('products:productkit-list')
except IntegrityError as e:
# Обработка нарушения уникальности в БД
error_msg = str(e).lower()
if 'unique_active_kit_name' in error_msg or ('name' in error_msg and 'duplicate' in error_msg):
messages.error(
self.request,
f'Ошибка: комплект с названием "{form.instance.name}" уже существует. '
'Пожалуйста, используйте другое название.'
)
elif 'slug' in error_msg or 'unique' in error_msg:
messages.error(
self.request,
'Ошибка: комплект с таким названием уже существует. '
'Пожалуйста, используйте другое название.'
)
else:
messages.error(
self.request,
'Ошибка при сохранении комплекта. Пожалуйста, проверьте введённые данные.'
)
return self.form_invalid(form)
except Exception as e:
messages.error(self.request, f'Ошибка при сохранении: {str(e)}')
import traceback
traceback.print_exc()
return self.form_invalid(form)
def form_invalid(self, form):
# Получаем формсет для отображения ошибок
context = self.get_context_data(form=form)
return self.render_to_response(context)
def get_success_url(self):
return reverse_lazy('products:productkit-list')
class ProductKitDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView):
"""
View для просмотра деталей комплекта.
Показывает все компоненты, цены, фотографии.
"""
model = ProductKit
template_name = 'products/productkit_detail.html'
context_object_name = 'kit'
permission_required = 'products.view_productkit'
def get_queryset(self):
# Prefetch для оптимизации запросов
return super().get_queryset().prefetch_related(
'photos',
'kit_items__product',
'kit_items__variant_group',
'tags'
)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# Добавляем фотографии комплекта в контекст
context['productkit_photos'] = self.object.photos.all().order_by('order', 'created_at')
context['photos_count'] = self.object.photos.count()
# Добавляем компоненты
context['kit_items'] = self.object.kit_items.all().select_related('product', 'variant_group')
return context
class ProductKitDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteView):
"""
View для удаления комплекта.
"""
model = ProductKit
template_name = 'products/productkit_confirm_delete.html'
context_object_name = 'kit'
permission_required = 'products.delete_productkit'
def get_success_url(self):
messages.success(self.request, f'Комплект "{self.object.name}" успешно удален!')
return reverse_lazy('products:productkit-list')
class ProductKitMakePermanentView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
"""
View для преобразования временного комплекта в постоянный.
Позволяет отредактировать название, добавить категории, теги перед сохранением.
"""
model = ProductKit
template_name = 'products/productkit_make_permanent.html'
context_object_name = 'kit'
permission_required = 'products.change_productkit'
fields = ['name', 'description', 'categories', 'tags', 'sale_price']
def get_queryset(self):
# Только временные комплекты можно преобразовать
return super().get_queryset().filter(is_temporary=True)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['kit_items'] = self.object.kit_items.all().select_related('product', 'variant_group')
context['productkit_photos'] = self.object.photos.all().order_by('order', 'created_at')
return context
def form_valid(self, form):
# Преобразуем в постоянный
if self.object.make_permanent():
messages.success(
self.request,
f'Комплект "{self.object.name}" преобразован в постоянный и теперь доступен в каталоге!'
)
else:
messages.warning(self.request, f'Комплект "{self.object.name}" уже является постоянным.')
return super().form_valid(form)
def get_success_url(self):
return reverse_lazy('products:productkit-detail', kwargs={'pk': self.object.pk})