Исправление отображения фото в POS и улучшение обработки изображений

- Исправлен POS для использования миниатюр вместо оригиналов для быстрой загрузки
- Убран fallback на оригиналы - показываем миниатюру или ничего (лучше видно ошибки)
- Исправлен ImageService - возвращает пустую строку если миниатюра обработанного файла не найдена
- Исправлена ошибка JavaScript при массовом удалении фото (insertAdjacentElement на null)
- Добавлен контейнер photos-messages-container для надежного отображения сообщений
- Улучшено логирование ImageService для отладки путей к файлам
- Добавлена проверка exists() с детальным логированием в TenantAwareFileSystemStorage
This commit is contained in:
2025-12-21 19:52:55 +03:00
parent 812ecb53e6
commit bb821f9ef4
4 changed files with 76 additions and 12 deletions

View File

@@ -99,7 +99,12 @@ def get_showcase_kits_for_pos():
for photo in photos: for photo in photos:
if photo.kit_id not in kit_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 = [] showcase_kits = []
@@ -776,7 +781,12 @@ def get_items_api(request):
for p in products_qs: for p in products_qs:
image_url = None image_url = None
if hasattr(p, 'first_photo_list') and p.first_photo_list: 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 available = p.available_qty
reserved = p.reserved_qty reserved = p.reserved_qty
@@ -827,7 +837,12 @@ def get_items_api(request):
for k in kits_qs: for k in kits_qs:
image_url = None image_url = None
if hasattr(k, 'first_photo_list') and k.first_photo_list: 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({ kits.append({
'id': k.id, 'id': k.id,
@@ -893,8 +908,15 @@ def get_product_kit_details(request, kit_id):
'price': str(ki.product.actual_price) 'price': str(ki.product.actual_price)
} for ki in kit.kit_items.all()] } 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({ return JsonResponse({
'success': True, 'success': True,

View File

@@ -164,6 +164,9 @@
</h5> </h5>
</div> </div>
<div class="card-body"> <div class="card-body">
<!-- Контейнер для сообщений об операциях с фото -->
<div id="photos-messages-container"></div>
<!-- Существующие фотографии (только при редактировании) --> <!-- Существующие фотографии (только при редактировании) -->
{% if object and product_photos %} {% if object and product_photos %}
<div class="mb-4"> <div class="mb-4">
@@ -342,7 +345,10 @@
// Скрываем блок если фотографий больше нет // Скрываем блок если фотографий больше нет
if (newCount === 0) { 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.className = 'alert alert-success alert-dismissible fade show';
alert.innerHTML = `${data.deleted} фото успешно удалено! alert.innerHTML = `${data.deleted} фото успешно удалено!
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>`; <button type="button" class="btn-close" data-bs-dismiss="alert"></button>`;
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'; deleteBtn.style.display = 'none';

View File

@@ -3,9 +3,12 @@
Используется в шаблонах и представлениях для удобного доступа к разным версиям. Используется в шаблонах и представлениях для удобного доступа к разным версиям.
""" """
import os import os
import logging
from django.conf import settings from django.conf import settings
from django.core.files.storage import default_storage from django.core.files.storage import default_storage
logger = logging.getLogger(__name__)
class ImageService: class ImageService:
""" """
@@ -63,7 +66,7 @@ class ImageService:
По умолчанию 'medium' По умолчанию 'medium'
Returns: Returns:
str: URL изображения или пустая строка если нет файла str: URL изображения нужного размера, или оригинал если размер не найден
""" """
if not original_image_path: if not original_image_path:
return '' return ''
@@ -80,6 +83,10 @@ class ImageService:
if 'temp' in parts: if 'temp' in parts:
return default_storage.url(path_str) return default_storage.url(path_str)
# Если запрашивается оригинал, возвращаем его напрямую
if size == 'original':
return default_storage.url(path_str)
# Извлекаем base_path, entity_id, photo_id из пути # Извлекаем base_path, entity_id, photo_id из пути
base_path = parts[0] # products, kits, categories base_path = parts[0] # products, kits, categories
entity_id = parts[1] # ID сущности entity_id = parts[1] # ID сущности
@@ -98,10 +105,25 @@ class ImageService:
# Используем default_storage.url() для корректной работы с TenantAwareFileSystemStorage # Используем default_storage.url() для корректной работы с TenantAwareFileSystemStorage
# Это гарантирует что URL будет содержать tenant_id если необходимо # Это гарантирует что 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: except Exception as e:
return '' # В случае ошибки возвращаем оригинал
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 @staticmethod
def get_thumbnail_url(original_image_path): def get_thumbnail_url(original_image_path):

View File

@@ -204,8 +204,15 @@ class TenantAwareFileSystemStorage(FileSystemStorage):
# Иначе добавляем tenant_id # Иначе добавляем tenant_id
tenant_aware_name = self._get_tenant_path(name) tenant_aware_name = self._get_tenant_path(name)
# Получаем полный путь для отладки
full_path = super().path(tenant_aware_name)
result = super().exists(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 return result
def url(self, name): def url(self, name):