Исправление отображения фото в POS и улучшение обработки изображений
- Исправлен POS для использования миниатюр вместо оригиналов для быстрой загрузки - Убран fallback на оригиналы - показываем миниатюру или ничего (лучше видно ошибки) - Исправлен ImageService - возвращает пустую строку если миниатюра обработанного файла не найдена - Исправлена ошибка JavaScript при массовом удалении фото (insertAdjacentElement на null) - Добавлен контейнер photos-messages-container для надежного отображения сообщений - Улучшено логирование ImageService для отладки путей к файлам - Добавлена проверка exists() с детальным логированием в TenantAwareFileSystemStorage
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -164,6 +164,9 @@
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<!-- Контейнер для сообщений об операциях с фото -->
|
||||
<div id="photos-messages-container"></div>
|
||||
|
||||
<!-- Существующие фотографии (только при редактировании) -->
|
||||
{% if object and product_photos %}
|
||||
<div class="mb-4">
|
||||
@@ -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} фото успешно удалено!
|
||||
<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';
|
||||
|
||||
@@ -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,8 +105,23 @@ 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 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 ''
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user