From bb821f9ef49535abba5244b3e1e2c9cb6a9fea4b Mon Sep 17 00:00:00 2001 From: Andrey Smakotin Date: Sun, 21 Dec 2025 19:52:55 +0300 Subject: [PATCH] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BE=D1=82=D0=BE=D0=B1=D1=80=D0=B0?= =?UTF-8?q?=D0=B6=D0=B5=D0=BD=D0=B8=D1=8F=20=D1=84=D0=BE=D1=82=D0=BE=20?= =?UTF-8?q?=D0=B2=20POS=20=D0=B8=20=D1=83=D0=BB=D1=83=D1=87=D1=88=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D0=BE=D0=B1=D1=80=D0=B0=D0=B1=D0=BE=D1=82?= =?UTF-8?q?=D0=BA=D0=B8=20=D0=B8=D0=B7=D0=BE=D0=B1=D1=80=D0=B0=D0=B6=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Исправлен POS для использования миниатюр вместо оригиналов для быстрой загрузки - Убран fallback на оригиналы - показываем миниатюру или ничего (лучше видно ошибки) - Исправлен ImageService - возвращает пустую строку если миниатюра обработанного файла не найдена - Исправлена ошибка JavaScript при массовом удалении фото (insertAdjacentElement на null) - Добавлен контейнер photos-messages-container для надежного отображения сообщений - Улучшено логирование ImageService для отладки путей к файлам - Добавлена проверка exists() с детальным логированием в TenantAwareFileSystemStorage --- myproject/pos/views.py | 32 ++++++++++++++++--- .../templates/products/product_form.html | 17 ++++++++-- myproject/products/utils/image_service.py | 30 ++++++++++++++--- myproject/products/utils/storage.py | 9 +++++- 4 files changed, 76 insertions(+), 12 deletions(-) diff --git a/myproject/pos/views.py b/myproject/pos/views.py index e876d3d..51a9a5e 100644 --- a/myproject/pos/views.py +++ b/myproject/pos/views.py @@ -99,7 +99,12 @@ def get_showcase_kits_for_pos(): for photo in photos: if photo.kit_id not in kit_photos: - kit_photos[photo.kit_id] = photo.get_thumbnail_url() + if photo and photo.image: + thumbnail_url = photo.get_thumbnail_url() + # Если миниатюра не найдена, возвращаем None (без фото) + if not thumbnail_url: + thumbnail_url = None + kit_photos[photo.kit_id] = thumbnail_url # Формируем результат showcase_kits = [] @@ -776,7 +781,12 @@ def get_items_api(request): for p in products_qs: image_url = None if hasattr(p, 'first_photo_list') and p.first_photo_list: - image_url = p.first_photo_list[0].get_thumbnail_url() + photo = p.first_photo_list[0] + if photo and photo.image: + image_url = photo.get_thumbnail_url() + # Если миниатюра не найдена, возвращаем None (без фото) + if not image_url: + image_url = None available = p.available_qty reserved = p.reserved_qty @@ -827,7 +837,12 @@ def get_items_api(request): for k in kits_qs: image_url = None if hasattr(k, 'first_photo_list') and k.first_photo_list: - image_url = k.first_photo_list[0].get_thumbnail_url() + photo = k.first_photo_list[0] + if photo and photo.image: + image_url = photo.get_thumbnail_url() + # Если миниатюра не найдена, возвращаем None (без фото) + if not image_url: + image_url = None kits.append({ 'id': k.id, @@ -893,8 +908,15 @@ def get_product_kit_details(request, kit_id): 'price': str(ki.product.actual_price) } for ki in kit.kit_items.all()] - # Фото - photo_url = kit.photos.first().image.url if kit.photos.exists() else None + # Фото (используем миниатюру для быстрой загрузки) + photo_url = None + if kit.photos.exists(): + first_photo = kit.photos.first() + if first_photo and first_photo.image: + photo_url = first_photo.get_thumbnail_url() + # Если миниатюра не найдена, возвращаем None (без фото) + if not photo_url: + photo_url = None return JsonResponse({ 'success': True, diff --git a/myproject/products/templates/products/product_form.html b/myproject/products/templates/products/product_form.html index 3161e8f..4e9c449 100644 --- a/myproject/products/templates/products/product_form.html +++ b/myproject/products/templates/products/product_form.html @@ -164,6 +164,9 @@
+ +
+ {% if object and product_photos %}
@@ -342,7 +345,10 @@ // Скрываем блок если фотографий больше нет if (newCount === 0) { - document.querySelector('[id="photos-count"]').closest('.mb-3').parentElement.style.display = 'none'; + const photosSection = document.querySelector('#photos-grid')?.closest('.mb-4'); + if (photosSection) { + photosSection.style.display = 'none'; + } } // Показываем сообщение об успехе @@ -350,7 +356,14 @@ alert.className = 'alert alert-success alert-dismissible fade show'; alert.innerHTML = `✓ ${data.deleted} фото успешно удалено! `; - document.querySelector('.mb-4.p-3.bg-light.rounded').insertAdjacentElement('beforebegin', alert); + + // Вставляем сообщение в специальный контейнер, который всегда существует + const messagesContainer = document.getElementById('photos-messages-container'); + if (messagesContainer) { + // Очищаем предыдущие сообщения и добавляем новое + messagesContainer.innerHTML = ''; + messagesContainer.appendChild(alert); + } // Скрываем кнопку deleteBtn.style.display = 'none'; diff --git a/myproject/products/utils/image_service.py b/myproject/products/utils/image_service.py index d69a248..fea132c 100644 --- a/myproject/products/utils/image_service.py +++ b/myproject/products/utils/image_service.py @@ -3,9 +3,12 @@ Используется в шаблонах и представлениях для удобного доступа к разным версиям. """ import os +import logging from django.conf import settings from django.core.files.storage import default_storage +logger = logging.getLogger(__name__) + class ImageService: """ @@ -63,7 +66,7 @@ class ImageService: По умолчанию 'medium' Returns: - str: URL изображения или пустая строка если нет файла + str: URL изображения нужного размера, или оригинал если размер не найден """ if not original_image_path: return '' @@ -80,6 +83,10 @@ class ImageService: if 'temp' in parts: return default_storage.url(path_str) + # Если запрашивается оригинал, возвращаем его напрямую + if size == 'original': + return default_storage.url(path_str) + # Извлекаем base_path, entity_id, photo_id из пути base_path = parts[0] # products, kits, categories entity_id = parts[1] # ID сущности @@ -98,10 +105,25 @@ class ImageService: # Используем default_storage.url() для корректной работы с TenantAwareFileSystemStorage # Это гарантирует что URL будет содержать tenant_id если необходимо - return default_storage.url(file_path) + # Проверяем существование файла - если не найден, возвращаем пустую строку + # (для обработанных файлов миниатюра должна существовать) + if default_storage.exists(file_path): + url = default_storage.url(file_path) + logger.debug(f"[ImageService] Returning {size} URL: {file_path} -> {url}") + return url + else: + # Файл нужного размера не найден - возвращаем пустую строку + # (файл обработан, но миниатюра не создана - это ошибка) + logger.warning(f"[ImageService] {size} file not found: {file_path}, file should be processed") + return '' - except Exception: - return '' + except Exception as e: + # В случае ошибки возвращаем оригинал + logger.warning(f"[ImageService] Error getting {size} URL: {e}, using original as fallback") + try: + return default_storage.url(str(original_image_path)) + except Exception: + return '' @staticmethod def get_thumbnail_url(original_image_path): diff --git a/myproject/products/utils/storage.py b/myproject/products/utils/storage.py index b4ab294..874e80b 100644 --- a/myproject/products/utils/storage.py +++ b/myproject/products/utils/storage.py @@ -204,8 +204,15 @@ class TenantAwareFileSystemStorage(FileSystemStorage): # Иначе добавляем tenant_id tenant_aware_name = self._get_tenant_path(name) + # Получаем полный путь для отладки + full_path = super().path(tenant_aware_name) result = super().exists(tenant_aware_name) - logger.info(f"[Storage] exists: {name} → {tenant_aware_name} → {result}") + logger.info(f"[Storage] exists: {name} → {tenant_aware_name} → {full_path} → {result}") + # Дополнительная проверка через os.path.exists для отладки + import os + os_result = os.path.exists(full_path) + if result != os_result: + logger.warning(f"[Storage] Mismatch! storage.exists()={result}, os.path.exists()={os_result} for {full_path}") return result def url(self, name):