Пытаемся угомонить превьюшки
All checks were successful
Deploy MES Core / deploy (push) Successful in 10s
All checks were successful
Deploy MES Core / deploy (push) Successful in 10s
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -3,6 +3,7 @@
|
|||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
*$py.class
|
*$py.class
|
||||||
|
logs/
|
||||||
|
|
||||||
# C extensions
|
# C extensions
|
||||||
*.so
|
*.so
|
||||||
|
|||||||
@@ -25,9 +25,6 @@ env = environ.Env()
|
|||||||
env_file = os.path.join(BASE_DIR, ".env")
|
env_file = os.path.join(BASE_DIR, ".env")
|
||||||
if os.path.exists(env_file):
|
if os.path.exists(env_file):
|
||||||
environ.Env.read_env(env_file)
|
environ.Env.read_env(env_file)
|
||||||
print(f"Файл .env найден и прочитан: {env_file}")
|
|
||||||
else:
|
|
||||||
print(f"ОШИБКА: Файл .env не найден по пути: {env_file}")
|
|
||||||
|
|
||||||
# читаем переменную окружения
|
# читаем переменную окружения
|
||||||
ENV_TYPE = os.getenv('ENV_TYPE', 'local')
|
ENV_TYPE = os.getenv('ENV_TYPE', 'local')
|
||||||
@@ -197,12 +194,4 @@ SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
|
|||||||
CSRF_TRUSTED_ORIGINS = env.list('CSRF_ORIGINS', default=['http://localhost'])
|
CSRF_TRUSTED_ORIGINS = env.list('CSRF_ORIGINS', default=['http://localhost'])
|
||||||
|
|
||||||
|
|
||||||
print(f"--- РАБОТАЕМ НА БАЗЕ: {DATABASES['default']['NAME']} (HOST: {DATABASES['default'].get('HOST', 'localhost')}) ---")
|
|
||||||
|
|
||||||
|
|
||||||
# Проверяем, видит ли он базу и режим отладки
|
|
||||||
print(f"DB_NAME: {env('DB_NAME', default='НЕ НАЙДЕНО')}")
|
|
||||||
print(f"ENV_TYPE: {env('ENV_TYPE', default='False')}")
|
|
||||||
print(f"SECRET_KEY: {env('SECRET_KEY', default='False')}")
|
|
||||||
print(f"CSRF_TRUSTED_ORIGINS: {CSRF_TRUSTED_ORIGINS}")
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
|
import logging
|
||||||
import multiprocessing
|
import multiprocessing
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
from django.db import close_old_connections
|
from django.db import close_old_connections
|
||||||
@@ -56,11 +58,25 @@ class Command(BaseCommand):
|
|||||||
job.started_at = timezone.now()
|
job.started_at = timezone.now()
|
||||||
job.finished_at = None
|
job.finished_at = None
|
||||||
job.last_message = ""
|
job.last_message = ""
|
||||||
|
try:
|
||||||
|
job.pid = os.getpid()
|
||||||
|
job.save(update_fields=["status", "started_at", "finished_at", "last_message", "pid"])
|
||||||
|
except Exception:
|
||||||
job.save(update_fields=["status", "started_at", "finished_at", "last_message"])
|
job.save(update_fields=["status", "started_at", "finished_at", "last_message"])
|
||||||
|
|
||||||
|
logger = logging.getLogger('dxf_preview_job')
|
||||||
|
if not logger.handlers:
|
||||||
|
handler = logging.StreamHandler(sys.stdout)
|
||||||
|
handler.setFormatter(logging.Formatter('%(asctime)s %(levelname)s %(message)s', '%Y-%m-%d %H:%M:%S'))
|
||||||
|
logger.addHandler(handler)
|
||||||
|
logger.setLevel(logging.INFO)
|
||||||
|
|
||||||
|
logger.info('start job=%s pid=%s', job_id, os.getpid())
|
||||||
|
|
||||||
# Берём настройки таймаута из БД.
|
# Берём настройки таймаута из БД.
|
||||||
settings, _ = DxfPreviewSettings.objects.get_or_create(pk=1)
|
settings, _ = DxfPreviewSettings.objects.get_or_create(pk=1)
|
||||||
per_task_timeout = int(getattr(settings, 'per_task_timeout_sec', 45) or 45)
|
per_task_timeout = int(getattr(settings, 'per_task_timeout_sec', 45) or 45)
|
||||||
|
per_task_timeout = max(10, min(50, per_task_timeout))
|
||||||
|
|
||||||
deal_statuses = ["lead", "work"]
|
deal_statuses = ["lead", "work"]
|
||||||
qs = ProductionTask.objects.select_related("deal").filter(deal__status__in=deal_statuses)
|
qs = ProductionTask.objects.select_related("deal").filter(deal__status__in=deal_statuses)
|
||||||
@@ -79,9 +95,7 @@ class Command(BaseCommand):
|
|||||||
skipped = 0
|
skipped = 0
|
||||||
errors = 0
|
errors = 0
|
||||||
|
|
||||||
# Таймаут обработки одной детали (сек).
|
logger.info('per_task_timeout=%ss', per_task_timeout)
|
||||||
# Если конкретный DXF «залип» — задача не должна блокироваться навсегда.
|
|
||||||
per_task_timeout = 45
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
for task in qs.iterator(chunk_size=50):
|
for task in qs.iterator(chunk_size=50):
|
||||||
@@ -106,7 +120,8 @@ class Command(BaseCommand):
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Обрабатываем одну деталь в отдельном процессе и ждём не больше per_task_timeout.
|
# Обрабатываем одну деталь в отдельном процессе и ждём не больше per_task_timeout.
|
||||||
close_old_connections()
|
# Важно: НЕ вызываем close_old_connections() внутри qs.iterator(), иначе Django может закрыть курсор,
|
||||||
|
# и итерация по QuerySet упадёт с ошибкой "cursor already closed".
|
||||||
q: multiprocessing.Queue = multiprocessing.Queue(maxsize=1)
|
q: multiprocessing.Queue = multiprocessing.Queue(maxsize=1)
|
||||||
p = multiprocessing.Process(target=_run_one_task_preview, args=(task.id, q))
|
p = multiprocessing.Process(target=_run_one_task_preview, args=(task.id, q))
|
||||||
p.start()
|
p.start()
|
||||||
@@ -137,8 +152,7 @@ class Command(BaseCommand):
|
|||||||
skipped += 1
|
skipped += 1
|
||||||
else:
|
else:
|
||||||
errors += 1
|
errors += 1
|
||||||
|
logger.error('error task=%s name=%s deal=%s: %s', task.id, task.drawing_name, task.deal.number, payload)
|
||||||
close_old_connections()
|
|
||||||
|
|
||||||
DxfPreviewJob.objects.filter(pk=job_id).update(
|
DxfPreviewJob.objects.filter(pk=job_id).update(
|
||||||
processed=processed,
|
processed=processed,
|
||||||
@@ -153,10 +167,11 @@ class Command(BaseCommand):
|
|||||||
last_message=f"Готово. Обновлены: {updated}. Пропущено: {skipped}. Ошибок: {errors}.",
|
last_message=f"Готово. Обновлены: {updated}. Пропущено: {skipped}. Ошибок: {errors}.",
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
logger.exception('fatal error')
|
||||||
DxfPreviewJob.objects.filter(pk=job_id).update(
|
DxfPreviewJob.objects.filter(pk=job_id).update(
|
||||||
status="failed",
|
status="failed",
|
||||||
finished_at=timezone.now(),
|
finished_at=timezone.now(),
|
||||||
last_message="Задача завершилась с ошибкой (см. логи процесса).",
|
last_message="Задача завершилась с ошибкой (см. лог на странице обслуживания).",
|
||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
close_old_connections()
|
close_old_connections()
|
||||||
@@ -22,6 +22,9 @@
|
|||||||
<div class="small text-muted">Обработано: <span id="jobProcessed">{% if last_job %}{{ last_job.processed }}{% endif %}</span>/<span id="jobTotal">{% if last_job %}{{ last_job.total }}{% endif %}</span></div>
|
<div class="small text-muted">Обработано: <span id="jobProcessed">{% if last_job %}{{ last_job.processed }}{% endif %}</span>/<span id="jobTotal">{% if last_job %}{{ last_job.total }}{% endif %}</span></div>
|
||||||
<div class="small text-muted">Обновлено: <span id="jobUpdated">{% if last_job %}{{ last_job.updated }}{% endif %}</span> · Пропущено: <span id="jobSkipped">{% if last_job %}{{ last_job.skipped }}{% endif %}</span> · Ошибок: <span id="jobErrors">{% if last_job %}{{ last_job.errors }}{% endif %}</span></div>
|
<div class="small text-muted">Обновлено: <span id="jobUpdated">{% if last_job %}{{ last_job.updated }}{% endif %}</span> · Пропущено: <span id="jobSkipped">{% if last_job %}{{ last_job.skipped }}{% endif %}</span> · Ошибок: <span id="jobErrors">{% if last_job %}{{ last_job.errors }}{% endif %}</span></div>
|
||||||
<div class="small text-muted" id="jobMessage">{% if last_job %}{{ last_job.last_message }}{% endif %}</div>
|
<div class="small text-muted" id="jobMessage">{% if last_job %}{{ last_job.last_message }}{% endif %}</div>
|
||||||
|
<div class="small text-muted mt-2">Лог</div>
|
||||||
|
<div class="small text-muted" id="jobLogPath">{% if last_job and last_job.log_path %}Файл: {{ last_job.log_path }}{% endif %}</div>
|
||||||
|
<pre id="jobLog" class="border border-secondary rounded p-2 mb-0" style="max-height: 220px; overflow:auto; white-space: pre-wrap;">{% if last_job %}{{ last_job.log_tail }}{% endif %}</pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form method="post" class="row g-3 align-items-end">
|
<form method="post" class="row g-3 align-items-end">
|
||||||
@@ -49,9 +52,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-3">
|
<div class="col-md-6">
|
||||||
<label class="small text-muted">Таймаут на 1 DXF (сек)</label>
|
<label class="small text-muted d-flex justify-content-between">
|
||||||
<input type="number" step="1" min="5" name="per_task_timeout_sec" class="form-control border-secondary" value="{{ dxf_settings.per_task_timeout_sec|default_if_none:45|unlocalize }}">
|
<span>Таймаут на 1 DXF (сек)</span>
|
||||||
|
<span id="timeoutValue">{{ dxf_settings.per_task_timeout_sec|default_if_none:45|unlocalize }}</span>
|
||||||
|
</label>
|
||||||
|
<input type="range" min="10" max="50" step="1" name="per_task_timeout_sec" id="timeoutRange" class="form-range" value="{{ dxf_settings.per_task_timeout_sec|default_if_none:45|unlocalize }}">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-12 d-flex gap-2">
|
<div class="col-12 d-flex gap-2">
|
||||||
@@ -64,6 +70,9 @@
|
|||||||
<button type="submit" class="btn btn-outline-warning" name="action" value="cancel_job">
|
<button type="submit" class="btn btn-outline-warning" name="action" value="cancel_job">
|
||||||
<i class="bi bi-stop-circle me-2"></i>Прервать
|
<i class="bi bi-stop-circle me-2"></i>Прервать
|
||||||
</button>
|
</button>
|
||||||
|
<button type="submit" class="btn btn-outline-secondary" name="action" value="clear_log">
|
||||||
|
<i class="bi bi-eraser me-2"></i>Очистить лог
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
@@ -93,6 +102,17 @@
|
|||||||
const skippedEl = document.getElementById('jobSkipped');
|
const skippedEl = document.getElementById('jobSkipped');
|
||||||
const errorsEl = document.getElementById('jobErrors');
|
const errorsEl = document.getElementById('jobErrors');
|
||||||
const msgEl = document.getElementById('jobMessage');
|
const msgEl = document.getElementById('jobMessage');
|
||||||
|
const logEl = document.getElementById('jobLog');
|
||||||
|
const logPathEl = document.getElementById('jobLogPath');
|
||||||
|
const timeoutRange = document.getElementById('timeoutRange');
|
||||||
|
const timeoutValue = document.getElementById('timeoutValue');
|
||||||
|
|
||||||
|
if (timeoutRange && timeoutValue) {
|
||||||
|
timeoutValue.textContent = timeoutRange.value;
|
||||||
|
timeoutRange.addEventListener('input', function () {
|
||||||
|
timeoutValue.textContent = timeoutRange.value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function tick() {
|
async function tick() {
|
||||||
try {
|
try {
|
||||||
@@ -109,6 +129,8 @@
|
|||||||
if (skippedEl) skippedEl.textContent = String(data.job.skipped ?? '');
|
if (skippedEl) skippedEl.textContent = String(data.job.skipped ?? '');
|
||||||
if (errorsEl) errorsEl.textContent = String(data.job.errors ?? '');
|
if (errorsEl) errorsEl.textContent = String(data.job.errors ?? '');
|
||||||
if (msgEl) msgEl.textContent = data.job.last_message || '';
|
if (msgEl) msgEl.textContent = data.job.last_message || '';
|
||||||
|
if (logEl) logEl.textContent = data.job.log_tail || '';
|
||||||
|
if (logPathEl) logPathEl.textContent = data.job.log_path ? `Файл: ${data.job.log_path}` : '';
|
||||||
|
|
||||||
if (data.job.status === 'running' || data.job.status === 'queued') {
|
if (data.job.status === 'running' || data.job.status === 'queued') {
|
||||||
setTimeout(tick, 3000);
|
setTimeout(tick, 3000);
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ from urllib.parse import urlsplit
|
|||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from django.conf import settings as django_settings
|
||||||
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.core.files.base import ContentFile
|
from django.core.files.base import ContentFile
|
||||||
@@ -169,7 +172,12 @@ def _update_task_preview(task: ProductionTask) -> bool:
|
|||||||
|
|
||||||
name = (task.drawing_file.name or '').lower()
|
name = (task.drawing_file.name or '').lower()
|
||||||
if not name.endswith('.dxf'):
|
if not name.endswith('.dxf'):
|
||||||
# Если не DXF — превью не делаем и очищаем габариты
|
# Если не DXF — превью не делаем: удаляем старый PNG (если был) и очищаем габариты.
|
||||||
|
try:
|
||||||
|
if task.preview_image:
|
||||||
|
task.preview_image.delete(save=False)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
task.preview_image = None
|
task.preview_image = None
|
||||||
task.blank_dimensions = ''
|
task.blank_dimensions = ''
|
||||||
task.save(update_fields=['preview_image', 'blank_dimensions'])
|
task.save(update_fields=['preview_image', 'blank_dimensions'])
|
||||||
@@ -184,16 +192,20 @@ def _update_task_preview(task: ProductionTask) -> bool:
|
|||||||
min_lineweight_mm=settings.min_lineweight,
|
min_lineweight_mm=settings.min_lineweight,
|
||||||
keep_original_colors=settings.keep_original_colors,
|
keep_original_colors=settings.keep_original_colors,
|
||||||
)
|
)
|
||||||
dims = ''
|
# Временно отключаем вычисление габаритов (bbox), чтобы исключить его влияние на стабильность.
|
||||||
|
# Превью PNG генерируем как и раньше.
|
||||||
|
|
||||||
|
# Перед сохранением удаляем старое превью, иначе FileSystemStorage добавляет суффиксы
|
||||||
|
# и папка с превью постепенно «засоряется».
|
||||||
try:
|
try:
|
||||||
dims = _extract_dxf_dimensions(dxf_path)
|
if task.preview_image:
|
||||||
|
task.preview_image.delete(save=False)
|
||||||
except Exception:
|
except Exception:
|
||||||
dims = ''
|
pass
|
||||||
|
|
||||||
filename = f"task_{task.id}_preview.png"
|
filename = f"task_{task.id}_preview.png"
|
||||||
task.preview_image.save(filename, ContentFile(png_bytes), save=False)
|
task.preview_image.save(filename, ContentFile(png_bytes), save=False)
|
||||||
task.blank_dimensions = dims
|
task.save(update_fields=['preview_image'])
|
||||||
task.save(update_fields=['preview_image', 'blank_dimensions'])
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# Класс главной страницы (роутер)
|
# Класс главной страницы (роутер)
|
||||||
@@ -644,6 +656,34 @@ def _mark_stale_preview_jobs() -> None:
|
|||||||
job.save(update_fields=['status', 'finished_at', 'last_message'])
|
job.save(update_fields=['status', 'finished_at', 'last_message'])
|
||||||
|
|
||||||
|
|
||||||
|
def _dxf_job_log_path(job_id: int) -> Path:
|
||||||
|
"""Путь к лог-файлу фоновой задачи DXF превью."""
|
||||||
|
base_dir = Path(getattr(django_settings, 'BASE_DIR', Path(__file__).resolve().parent.parent))
|
||||||
|
logs_dir = base_dir / 'logs'
|
||||||
|
logs_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
return logs_dir / f'dxf_preview_job_{job_id}.log'
|
||||||
|
|
||||||
|
|
||||||
|
def _read_tail(path: Path, max_bytes: int = 32_000) -> str:
|
||||||
|
"""Читает «хвост» файла (последние max_bytes) для вывода в UI."""
|
||||||
|
try:
|
||||||
|
if not path.exists():
|
||||||
|
return ''
|
||||||
|
with path.open('rb') as f:
|
||||||
|
f.seek(0, os.SEEK_END)
|
||||||
|
size = f.tell()
|
||||||
|
start = max(0, size - max_bytes)
|
||||||
|
f.seek(start)
|
||||||
|
data = f.read()
|
||||||
|
if start > 0:
|
||||||
|
nl = data.find(b'\n')
|
||||||
|
if nl != -1:
|
||||||
|
data = data[nl + 1 :]
|
||||||
|
return data.decode('utf-8', errors='replace')
|
||||||
|
except Exception:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
|
||||||
class MaintenanceStatusView(LoginRequiredMixin, View):
|
class MaintenanceStatusView(LoginRequiredMixin, View):
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
profile = getattr(request.user, 'profile', None)
|
profile = getattr(request.user, 'profile', None)
|
||||||
@@ -651,12 +691,13 @@ class MaintenanceStatusView(LoginRequiredMixin, View):
|
|||||||
if role != 'admin':
|
if role != 'admin':
|
||||||
return JsonResponse({'error': 'forbidden'}, status=403)
|
return JsonResponse({'error': 'forbidden'}, status=403)
|
||||||
|
|
||||||
# Перед отдачей статуса пытаемся снять «залипшие» задачи.
|
|
||||||
_mark_stale_preview_jobs()
|
_mark_stale_preview_jobs()
|
||||||
|
|
||||||
job = DxfPreviewJob.objects.order_by('-id').first()
|
job = DxfPreviewJob.objects.order_by('-id').first()
|
||||||
if not job:
|
if not job:
|
||||||
return JsonResponse({'job': None})
|
return JsonResponse({'job': None, 'log_tail': ''})
|
||||||
|
|
||||||
|
log_tail = _read_tail(_dxf_job_log_path(job.id))
|
||||||
|
|
||||||
return JsonResponse({
|
return JsonResponse({
|
||||||
'job': {
|
'job': {
|
||||||
@@ -668,8 +709,10 @@ class MaintenanceStatusView(LoginRequiredMixin, View):
|
|||||||
'updated': job.updated,
|
'updated': job.updated,
|
||||||
'skipped': job.skipped,
|
'skipped': job.skipped,
|
||||||
'errors': job.errors,
|
'errors': job.errors,
|
||||||
'cancel_requested': job.cancel_requested,
|
'cancel_requested': getattr(job, 'cancel_requested', False),
|
||||||
'last_message': job.last_message,
|
'last_message': job.last_message,
|
||||||
|
'log_tail': log_tail,
|
||||||
|
'log_path': str(_dxf_job_log_path(job.id)),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -739,6 +782,25 @@ class MaintenanceView(LoginRequiredMixin, TemplateView):
|
|||||||
messages.success(request, 'Остановка запрошена.')
|
messages.success(request, 'Остановка запрошена.')
|
||||||
return redirect('maintenance')
|
return redirect('maintenance')
|
||||||
|
|
||||||
|
if action == 'clear_log':
|
||||||
|
# Очистка лог-файла последней задачи. Во время выполнения не трогаем,
|
||||||
|
# потому что процесс может держать открытый дескриптор файла.
|
||||||
|
job = DxfPreviewJob.objects.order_by('-id').first()
|
||||||
|
if not job:
|
||||||
|
messages.info(request, 'Логов нет.')
|
||||||
|
return redirect('maintenance')
|
||||||
|
|
||||||
|
if job.status in ['queued', 'running'] and not job.finished_at:
|
||||||
|
messages.warning(request, 'Нельзя очистить лог во время выполнения задачи.')
|
||||||
|
return redirect('maintenance')
|
||||||
|
|
||||||
|
try:
|
||||||
|
_dxf_job_log_path(job.id).open('wb').close()
|
||||||
|
messages.success(request, 'Лог очищен.')
|
||||||
|
except Exception:
|
||||||
|
messages.error(request, 'Не удалось очистить лог.')
|
||||||
|
return redirect('maintenance')
|
||||||
|
|
||||||
if action != 'update_previews':
|
if action != 'update_previews':
|
||||||
messages.success(request, 'Настройки превью сохранены.')
|
messages.success(request, 'Настройки превью сохранены.')
|
||||||
return redirect('maintenance')
|
return redirect('maintenance')
|
||||||
@@ -758,14 +820,27 @@ class MaintenanceView(LoginRequiredMixin, TemplateView):
|
|||||||
job = DxfPreviewJob.objects.create(status='queued', created_by=request.user)
|
job = DxfPreviewJob.objects.create(status='queued', created_by=request.user)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
subprocess.Popen(
|
log_path = _dxf_job_log_path(job.id)
|
||||||
|
log_fh = log_path.open('ab')
|
||||||
|
try:
|
||||||
|
p = subprocess.Popen(
|
||||||
[sys.executable, 'manage.py', 'dxf_preview_job', str(job.id)],
|
[sys.executable, 'manage.py', 'dxf_preview_job', str(job.id)],
|
||||||
cwd=os.path.dirname(os.path.dirname(__file__)),
|
cwd=str(Path(__file__).resolve().parent.parent),
|
||||||
stdout=subprocess.DEVNULL,
|
stdout=log_fh,
|
||||||
stderr=subprocess.DEVNULL,
|
stderr=log_fh,
|
||||||
close_fds=True,
|
close_fds=True,
|
||||||
)
|
)
|
||||||
messages.success(request, 'Запущено обновление превью DXF в фоне. Прогресс обновляется ниже.')
|
finally:
|
||||||
|
log_fh.close()
|
||||||
|
|
||||||
|
# Если в модели есть поле pid — сохраняем его для диагностики.
|
||||||
|
try:
|
||||||
|
job.pid = p.pid
|
||||||
|
job.save(update_fields=['pid'])
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
messages.success(request, 'Запущено обновление превью DXF в фоне. Прогресс и лог обновляются ниже.')
|
||||||
except Exception:
|
except Exception:
|
||||||
job.status = 'failed'
|
job.status = 'failed'
|
||||||
job.last_message = 'Не удалось запустить фоновый процесс генерации превью.'
|
job.last_message = 'Не удалось запустить фоновый процесс генерации превью.'
|
||||||
|
|||||||
Reference in New Issue
Block a user