Улучшение системы работы с фото: добавлена команда очистки битых записей и оптимизация обработки изображений

This commit is contained in:
2026-01-06 09:25:37 +03:00
parent 0f19542ac9
commit 288716deba
14 changed files with 535 additions and 122 deletions

View File

@@ -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

View File

@@ -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)