Исправление отображения фото в 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:
|
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,
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
Reference in New Issue
Block a user