Files
octopus/myproject/static/js/photo-progress.js
Andrey Smakotin 0791ebb13b fix: Сохранять файл фото ДО запуска Celery task
При асинхронной обработке фото нужно сначала сохранить файл в БД,
потом запустить Celery task. Иначе task не найдет файл.

Изменения:
- BasePhoto.save() теперь сохраняет файл перед запуском task
- Исправлена проблема 'Photo has no image file' в Celery worker

🤖 Generated with Claude Code
2025-11-15 11:11:08 +03:00

310 lines
11 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Photo Processing Progress Tracking
*
* Отслеживает прогресс асинхронной обработки фото через Celery.
* Показывает прогресс-бар и обновляет UI по мере обработки.
*/
class PhotoProgressTracker {
constructor(options = {}) {
this.options = {
pollInterval: options.pollInterval || 2000, // 2 секунды
maxRetries: options.maxRetries || 5,
onSuccess: options.onSuccess || (() => {}),
onError: options.onError || (() => {}),
onProgress: options.onProgress || (() => {}),
};
this.activeTrackers = new Map(); // task_id -> status
}
/**
* Запустить отслеживание одного фото
*
* @param {string} taskId - ID задачи Celery
* @param {HTMLElement} photoElement - DOM элемент фото (опционально)
* @param {Function} onComplete - Callback при завершении
*/
trackPhoto(taskId, photoElement = null, onComplete = null) {
console.log(`[PhotoProgress] Tracking photo: ${taskId}`);
this.activeTrackers.set(taskId, {
status: 'pending',
progress: 0,
photoElement: photoElement,
onComplete: onComplete,
retries: 0,
});
// Запускаем периодическое опрашивание статуса
this._pollStatus(taskId);
}
/**
* Отслеживание нескольких фото одновременно
*
* @param {string[]} taskIds - Массив task_id
* @param {Function} onAllComplete - Callback когда все завершены
*/
trackMultiple(taskIds, onAllComplete = null) {
console.log(`[PhotoProgress] Tracking ${taskIds.length} photos`);
taskIds.forEach((taskId, index) => {
this.trackPhoto(taskId, null, () => {
// Проверяем все ли завершены
const allCompleted = taskIds.every(id => {
const tracker = this.activeTrackers.get(id);
return tracker && tracker.status === 'success';
});
if (allCompleted && onAllComplete) {
console.log('[PhotoProgress] All photos processed successfully');
onAllComplete();
}
});
});
}
/**
* Приватный метод для опрашивания статуса
*/
_pollStatus(taskId) {
const statusUrl = `/products/api/photos/status/${taskId}/`;
fetch(statusUrl)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
})
.then(data => {
this._handleStatusUpdate(taskId, data);
})
.catch(error => {
console.error(`[PhotoProgress] Error polling status for ${taskId}:`, error);
this._handleError(taskId, error);
});
}
/**
* Обработка обновления статуса
*/
_handleStatusUpdate(taskId, data) {
const tracker = this.activeTrackers.get(taskId);
if (!tracker) return;
const celeryStatus = data.status; // PENDING, STARTED, SUCCESS, FAILURE, RETRY
let progress = data.progress || 0;
let status = 'processing';
console.log(`[PhotoProgress] ${taskId}: ${celeryStatus} (${progress}%)`);
switch (celeryStatus) {
case 'PENDING':
progress = 0;
status = 'pending';
this._updateUI(tracker, progress, '⏳ В очереди на обработку...');
// Продолжаем опрашивать
setTimeout(() => this._pollStatus(taskId), this.options.pollInterval);
break;
case 'STARTED':
progress = 50;
status = 'processing';
this._updateUI(tracker, progress, '⚙️ Обрабатывается изображение...');
// Продолжаем опрашивать
setTimeout(() => this._pollStatus(taskId), this.options.pollInterval);
break;
case 'SUCCESS':
progress = 100;
status = 'success';
this._updateUI(tracker, progress, '✅ Обработано успешно');
tracker.status = 'success';
this.activeTrackers.set(taskId, tracker);
// Вызываем callback
if (tracker.onComplete) {
tracker.onComplete(data.result);
}
this.options.onSuccess(taskId, data.result);
break;
case 'FAILURE':
progress = 0;
status = 'failed';
this._updateUI(tracker, progress, '❌ Ошибка при обработке');
tracker.status = 'failed';
this.activeTrackers.set(taskId, tracker);
// Вызываем обработчик ошибки
this.options.onError(taskId, data.error || 'Unknown error');
break;
case 'RETRY':
progress = 25;
status = 'retrying';
this._updateUI(tracker, progress, '🔄 Повторная попытка...');
// Продолжаем опрашивать
setTimeout(() => this._pollStatus(taskId), this.options.pollInterval);
break;
default:
// Неизвестный статус, продолжаем опрашивать
setTimeout(() => this._pollStatus(taskId), this.options.pollInterval);
}
// Обновляем в кэше
tracker.status = status;
tracker.progress = progress;
this.activeTrackers.set(taskId, tracker);
// Вызываем callback прогресса
this.options.onProgress(taskId, progress, status);
}
/**
* Обновление UI элемента
*/
_updateUI(tracker, progress, message) {
if (!tracker.photoElement) return;
const element = tracker.photoElement;
// Обновляем/создаем прогресс-бар
let progressBar = element.querySelector('.photo-progress-bar');
if (!progressBar) {
progressBar = document.createElement('div');
progressBar.className = 'photo-progress-bar';
progressBar.innerHTML = `
<div class="progress-container">
<div class="progress-fill" style="width: 0%"></div>
</div>
<div class="progress-message"></div>
`;
element.appendChild(progressBar);
}
// Обновляем прогресс
const progressFill = progressBar.querySelector('.progress-fill');
progressFill.style.width = progress + '%';
// Обновляем сообщение
const messageEl = progressBar.querySelector('.progress-message');
messageEl.textContent = message;
// CSS стили
if (!document.getElementById('photo-progress-styles')) {
const style = document.createElement('style');
style.id = 'photo-progress-styles';
style.textContent = `
.photo-progress-bar {
margin-top: 10px;
padding: 10px;
background: #f5f5f5;
border-radius: 4px;
}
.progress-container {
width: 100%;
height: 24px;
background: #e0e0e0;
border-radius: 12px;
overflow: hidden;
margin-bottom: 8px;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #4CAF50 0%, #45a049 100%);
transition: width 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 12px;
font-weight: bold;
}
.progress-message {
font-size: 14px;
color: #666;
text-align: center;
}
`;
document.head.appendChild(style);
}
}
/**
* Обработка ошибки
*/
_handleError(taskId, error) {
const tracker = this.activeTrackers.get(taskId);
if (!tracker) return;
tracker.retries = (tracker.retries || 0) + 1;
if (tracker.retries < this.options.maxRetries) {
console.warn(`[PhotoProgress] Retrying ${taskId} (${tracker.retries}/${this.options.maxRetries})`);
setTimeout(() => this._pollStatus(taskId), this.options.pollInterval * 2);
} else {
console.error(`[PhotoProgress] Max retries exceeded for ${taskId}`);
tracker.status = 'failed';
this._updateUI(tracker, 0, '❌ Ошибка: превышено максимальное количество попыток');
this.options.onError(taskId, 'Max retries exceeded');
}
}
/**
* Остановить отслеживание фото
*/
stopTracking(taskId) {
this.activeTrackers.delete(taskId);
console.log(`[PhotoProgress] Stopped tracking ${taskId}`);
}
/**
* Получить статус фото
*/
getStatus(taskId) {
return this.activeTrackers.get(taskId);
}
}
// Глобальный экземпляр
window.photoProgressTracker = new PhotoProgressTracker({
pollInterval: 2000,
onSuccess: (taskId, result) => {
console.log(`[PhotoProgress] Photo ${taskId} processing completed:`, result);
},
onError: (taskId, error) => {
console.error(`[PhotoProgress] Photo ${taskId} error:`, error);
alert(`Ошибка при обработке фото: ${error}`);
},
onProgress: (taskId, progress, status) => {
// Можно использовать для обновления глобального статуса
}
});
/**
* Интеграция с Django формой загрузки фото
*
* Добавьте этот скрипт в шаблон формы создания товара:
*
* <form method="post" enctype="multipart/form-data" id="product-form">
* ...
* <input type="file" name="photos" multiple accept="image/*">
* ...
* </form>
*
* <script>
* document.getElementById('product-form').addEventListener('submit', function(e) {
* // После сохранения товара, если есть task_id в response
* // Можно запустить отслеживание:
* // window.photoProgressTracker.trackPhoto(taskId, photoElement);
* });
* </script>
*/