Улучшение системы работы с фото: добавлена команда очистки битых записей и оптимизация обработки изображений
This commit is contained in:
@@ -2,16 +2,19 @@
|
||||
Представление для каталога товаров и комплектов.
|
||||
"""
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.views.generic import TemplateView
|
||||
from django.db.models import Prefetch, Sum, Value, DecimalField
|
||||
from django.views.generic import ListView
|
||||
from django.db.models import Prefetch, Sum, Value, DecimalField, Q
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.core.paginator import Paginator
|
||||
|
||||
from ..models import Product, ProductKit, ProductCategory, ProductPhoto, ProductKitPhoto
|
||||
from ..models import Product, ProductKit, ProductCategory, ProductPhoto, ProductKitPhoto, KitItem
|
||||
|
||||
|
||||
class CatalogView(LoginRequiredMixin, TemplateView):
|
||||
"""Каталог с деревом категорий слева и сеткой товаров справа."""
|
||||
class CatalogView(LoginRequiredMixin, ListView):
|
||||
"""Каталог с деревом категорий слева и сеткой товаров справа с пагинацией."""
|
||||
template_name = 'products/catalog.html'
|
||||
context_object_name = 'items'
|
||||
paginate_by = 50
|
||||
|
||||
def build_category_tree(self, categories, parent=None):
|
||||
"""Рекурсивно строит дерево категорий с товарами."""
|
||||
@@ -25,94 +28,84 @@ class CatalogView(LoginRequiredMixin, TemplateView):
|
||||
})
|
||||
return tree
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
def get_queryset(self):
|
||||
"""Получаем объединенный список товаров и комплектов с оптимизацией."""
|
||||
# Аннотации для остатков
|
||||
total_available = Coalesce(Sum('stocks__quantity_available'), Value(0), output_field=DecimalField())
|
||||
total_reserved = Coalesce(Sum('stocks__quantity_reserved'), Value(0), output_field=DecimalField())
|
||||
|
||||
# Оптимизированный prefetch с аннотациями для активных товаров
|
||||
active_products_prefetch = Prefetch(
|
||||
'products',
|
||||
queryset=Product.objects.filter(status='active').prefetch_related(
|
||||
Prefetch('photos', queryset=ProductPhoto.objects.order_by('order'))
|
||||
).annotate(
|
||||
total_available=total_available,
|
||||
total_reserved=total_reserved,
|
||||
).order_by('name')
|
||||
)
|
||||
|
||||
# Оптимизированный prefetch для комплектов
|
||||
active_kits_prefetch = Prefetch(
|
||||
'kits',
|
||||
queryset=ProductKit.objects.filter(status='active', is_temporary=False).prefetch_related(
|
||||
Prefetch('photos', queryset=ProductKitPhoto.objects.order_by('order'))
|
||||
).order_by('name')
|
||||
# Prefetch только главного фото для товаров (is_main=True)
|
||||
main_product_photo = Prefetch(
|
||||
'photos',
|
||||
queryset=ProductPhoto.objects.filter(is_main=True),
|
||||
to_attr='main_photo_list'
|
||||
)
|
||||
|
||||
# Все активные категории с оптимизированным prefetch
|
||||
categories = list(ProductCategory.objects.filter(
|
||||
is_active=True, is_deleted=False
|
||||
).prefetch_related(active_products_prefetch, active_kits_prefetch).order_by('name'))
|
||||
|
||||
# Строим дерево
|
||||
category_tree = self.build_category_tree(categories, parent=None)
|
||||
|
||||
# Извлекаем товары и комплекты - два способа:
|
||||
# 1. Из категорий (для оптимизации prefetch)
|
||||
# 2. Все активные товары напрямую (для товаров без категорий)
|
||||
products_dict = {}
|
||||
kits_dict = {}
|
||||
|
||||
# Сначала извлекаем из категорий (используем prefetch кеш)
|
||||
for cat in categories:
|
||||
for p in cat.products.all():
|
||||
if p.id not in products_dict:
|
||||
p.item_type = 'product'
|
||||
p.main_photo = p.photos.all()[0] if p.photos.all() else None
|
||||
p.total_free = p.total_available - p.total_reserved
|
||||
products_dict[p.id] = p
|
||||
|
||||
for k in cat.kits.all():
|
||||
if k.id not in kits_dict:
|
||||
k.item_type = 'kit'
|
||||
k.main_photo = k.photos.all()[0] if k.photos.all() else None
|
||||
# Рассчитываем доступное количество комплектов
|
||||
k.total_free = k.calculate_available_quantity()
|
||||
kits_dict[k.id] = k
|
||||
|
||||
# Теперь добавляем все товары, которых еще нет (товары без категорий или не загруженные)
|
||||
all_products = Product.objects.filter(status='active').prefetch_related(
|
||||
Prefetch('photos', queryset=ProductPhoto.objects.order_by('order'))
|
||||
# Товары с фотографиями и остатками
|
||||
products = Product.objects.filter(status='active').prefetch_related(
|
||||
main_product_photo,
|
||||
'categories'
|
||||
).annotate(
|
||||
total_available=total_available,
|
||||
total_reserved=total_reserved,
|
||||
).order_by('name')
|
||||
|
||||
for p in all_products:
|
||||
if p.id not in products_dict:
|
||||
p.item_type = 'product'
|
||||
p.main_photo = p.photos.all()[0] if p.photos.all() else None
|
||||
p.total_free = p.total_available - p.total_reserved
|
||||
products_dict[p.id] = p
|
||||
|
||||
# Все комплекты
|
||||
all_kits = ProductKit.objects.filter(status='active', is_temporary=False).prefetch_related(
|
||||
Prefetch('photos', queryset=ProductKitPhoto.objects.order_by('order'))
|
||||
|
||||
# Prefetch только главного фото для комплектов (is_main=True)
|
||||
main_kit_photo = Prefetch(
|
||||
'photos',
|
||||
queryset=ProductKitPhoto.objects.filter(is_main=True),
|
||||
to_attr='main_photo_list'
|
||||
)
|
||||
|
||||
# Комплекты с фотографиями
|
||||
kits = ProductKit.objects.filter(status='active', is_temporary=False).prefetch_related(
|
||||
main_kit_photo,
|
||||
'categories',
|
||||
Prefetch(
|
||||
'kit_items',
|
||||
queryset=KitItem.objects.select_related(
|
||||
'product', 'variant_group'
|
||||
).prefetch_related('product__stocks')
|
||||
)
|
||||
).order_by('name')
|
||||
|
||||
# Объединяем товары и комплекты
|
||||
items_list = []
|
||||
|
||||
for k in all_kits:
|
||||
if k.id not in kits_dict:
|
||||
k.item_type = 'kit'
|
||||
k.main_photo = k.photos.all()[0] if k.photos.all() else None
|
||||
# Рассчитываем доступное количество комплектов
|
||||
k.total_free = k.calculate_available_quantity()
|
||||
kits_dict[k.id] = k
|
||||
for p in products:
|
||||
p.item_type = 'product'
|
||||
# Используем кешированное главное фото из prefetch
|
||||
p.cached_main_photo = p.main_photo_list[0] if p.main_photo_list else None
|
||||
# Кешируем категории для избежания повторных запросов
|
||||
p.cached_categories = list(p.categories.all())
|
||||
p.total_free = p.total_available - p.total_reserved
|
||||
items_list.append(p)
|
||||
|
||||
for k in kits:
|
||||
k.item_type = 'kit'
|
||||
# Используем кешированное главное фото из prefetch
|
||||
k.cached_main_photo = k.main_photo_list[0] if k.main_photo_list else None
|
||||
# Кешируем категории для избежания повторных запросов
|
||||
k.cached_categories = list(k.categories.all())
|
||||
# Кешируем результат calculate_available_quantity
|
||||
k.total_free = k.calculate_available_quantity()
|
||||
items_list.append(k)
|
||||
|
||||
# Объединяем и сортируем
|
||||
items = sorted(list(products_dict.values()) + list(kits_dict.values()), key=lambda x: x.name)
|
||||
# Сортируем по имени
|
||||
items_list.sort(key=lambda x: x.name)
|
||||
|
||||
return items_list
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
# Все активные категории для дерева
|
||||
categories = list(ProductCategory.objects.filter(
|
||||
is_active=True, is_deleted=False
|
||||
).order_by('name'))
|
||||
|
||||
# Строим дерево
|
||||
category_tree = self.build_category_tree(categories, parent=None)
|
||||
context['category_tree'] = category_tree
|
||||
context['items'] = items
|
||||
|
||||
return context
|
||||
|
||||
@@ -45,7 +45,10 @@ def generic_photo_delete(request, pk, photo_model, redirect_url_name, parent_att
|
||||
|
||||
def generic_photo_set_main(request, pk, photo_model, redirect_url_name, parent_attr, permission):
|
||||
"""
|
||||
Универсальная установка фото как главного (order = 0).
|
||||
Универсальная установка фото как главного (is_main=True).
|
||||
|
||||
Автоматически сбрасывает is_main=False у старого главного фото.
|
||||
Constraint на уровне БД гарантирует, что у сущности может быть только одно is_main=True.
|
||||
|
||||
Args:
|
||||
request: HTTP request
|
||||
@@ -64,24 +67,21 @@ def generic_photo_set_main(request, pk, photo_model, redirect_url_name, parent_a
|
||||
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:
|
||||
if photo.is_main:
|
||||
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()
|
||||
# Сбрасываем 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)
|
||||
|
||||
Reference in New Issue
Block a user