release 1.2

This commit is contained in:
2026-03-03 22:43:26 +03:00
parent 651631b3c7
commit 40dec27d41
27 changed files with 494 additions and 496 deletions
BIN
View File
Binary file not shown.
+1 -1
View File
@@ -196,7 +196,7 @@ async def getTrackingIDSigning(trackingID: str, singingId: int):
if not content:
continue
ATTACHMENTS_DIR = Path("src/attachments/import")
ATTACHMENTS_DIR = Path("attachments/import").resolve()
result = detect_and_save_attachment(
b64_content=content,
output_dir=ATTACHMENTS_DIR / str(singingId),
+2 -2
View File
@@ -524,8 +524,8 @@ async def search_recipients_post(
settings = await Settings.getSettings(False)
if not settings.success:
logger.error(settings.error)
return {"status": "error", "message": settings.error}
logger.error(settings.message)
return {"status": "error", "message": settings.message}
settings = settings.data
try:
patientsDB = await findPatientByPhone(
-1
View File
@@ -17,7 +17,6 @@ async def settingsPage(request: Request):
@router.get("/get", name="getSettings", summary="Получить настройки")
async def getSettings():
settings = await Settings.getSettings()
# logger.info(settings.data)
return settings.data
+7 -2
View File
@@ -364,7 +364,12 @@ function updateStaffTable() {
// Определяем тип подписи
let signatureTypeHtml;
if (practitioner.esiaAuth) {
signatureTypeHtml = '<span class="badge bg-success">УКЭП</span>';
const snils = (practitioner.snils || '').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
const snilsIcon = `<button type="button" class="btn btn-sm btn-link p-0 ms-1"
onclick="showAttorneyPopup('${snils}', this)">
<i class="bi bi-eye"></i>
</button>`;
signatureTypeHtml = `<span class="badge bg-success me-4">УКЭП</span><span class="badge bg-info">СНИЛС</span>${snilsIcon}`;
} else {
// Экранируем специальные символы в attorney
const attorney = (practitioner.attorney || '').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
@@ -914,7 +919,7 @@ document.addEventListener('DOMContentLoaded', function () {
const userIdLpu = document.getElementById('ukepPractitionerId').value;
const ukepFile = document.getElementById('ukepFile').files[0];
const expiryDate = document.getElementById('ukepExpiryDate').value;
const snils = document.getElementById('mchdSnilsNumber').value.replace(/\D/g, '');
const snils = document.getElementById('ukepSnilsNumber').value.replace(/\D/g, '');
if (!expiryDate) {
showAlert('Заполните дату окончания действия', 'warning');
-21
View File
@@ -1,21 +0,0 @@
{% extends "base.html" %}
<!-- Заголовок страницы -->
{% block title %}
{% endblock %}
<!-- Блок шапки -->
{% block head %}
{% endblock %}
<!-- Блок тела -->
{% block body %}
{% endblock %}
<!-- Блок скриптов -->
{% block scripts %}
{% endblock %}
+1 -1
View File
@@ -236,7 +236,7 @@
<div id="attorneyPopup" class="position-fixed bg-white border rounded shadow p-3"
style="display: none; z-index: 1060; max-width: 400px;">
<div class="d-flex justify-content-between align-items-start mb-2">
<h6 class="mb-0">Номер МЧД</h6>
<h6 class="mb-0">Номер:</h6>
<button type="button" class="btn-close btn-sm" id="closeAttorneyPopupBtn"></button>
</div>
<div class="mb-2">
BIN
View File
Binary file not shown.
Binary file not shown.
-87
View File
@@ -1,87 +0,0 @@
{
"201": {
"ru": "Создан",
"en": "Created",
"description": "Документ создан через API, готов к обработке"
},
"202": {
"ru": "Отправлен на подписание",
"en": "Sent for Signing",
"description": "Документ отправлен получателю"
},
"203": {
"ru": "Просмотрел",
"en": "Viewed",
"description": "Документ просмотрен получателем"
},
"204": {
"ru": "Подписал",
"en": "Signed",
"description": "Документ подписан получателем"
},
"205": {
"ru": "Отказался",
"en": "Rejected",
"description": "Получатель отказался от подписания"
},
"206": {
"ru": "Срок для подписания истек",
"en": "Signing Period Expired",
"description": "Срок действия ссылки истек до момента проставления подписи или отказа"
},
"207": {
"ru": "Ожидание действий всех получателей",
"en": "Awaiting Actions from All Recipients",
"description": "Ожидание действий от всех получателей"
},
"208": {
"ru": "Успешно. Подписан всеми — обработка",
"en": "Success. Fully Signed — Processing",
"description": "Все получатели подписали, документ обрабатывается"
},
"209": {
"ru": "Успешно. Подписан не всеми — обработка",
"en": "Success. Partially Signed — Processing",
"description": "Не все получатели подписали, документ обрабатывается"
},
"210": {
"ru": "Завершено. Подписано всеми",
"en": "Completed. Fully Signed",
"description": "Все получатели подписали, документ сохранен"
},
"211": {
"ru": "Завершено. Подписано не всеми",
"en": "Completed. Partially Signed",
"description": "Не все получатели подписали, документ сохранен"
},
"212": {
"ru": "Отменен для получателя",
"en": "Cancelled for Recipient",
"description": "Подписание отменено для конкретного получателя"
},
"213": {
"ru": "Завершено. Отменено для всех",
"en": "Completed. Cancelled for All",
"description": "Подписание отменено для всех получателей"
},
"498": {
"ru": "Завершено. Отклонено всеми",
"en": "Completed. Rejected by All",
"description": "Все получатели отказались от подписания"
},
"499": {
"ru": "Завершено. Истекло для всех",
"en": "Completed. Expired for All",
"description": "Срок ссылки истек для всех получателей"
},
"500": {
"ru": "Ошибка обработки",
"en": "Processing Error",
"description": "Ошибка во время обработки документа"
},
"501": {
"ru": "Ошибка доставки получателю",
"en": "Delivery Error",
"description": "Произошла ошибка при доставке конкретному получателю"
}
}
+1
View File
@@ -43,6 +43,7 @@ class DatabaseInitializer:
if not tables_exist:
logger.warning("Не все таблицы существуют. Создаем недостающие...")
await self._create_tables_directly()
await self._initialize_data()
# 🔥 СИНХРОНИЗАЦИЯ СХЕМЫ
logger.info("Синхронизация схемы БД...")
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+2 -21
View File
@@ -5,27 +5,14 @@ from contextlib import asynccontextmanager
from app.routers import router
import config
from utils import logger
from utils import logger
from utils.background import init_scheduler, scheduler_lifespan
# Инициализация задач планировщика (без запуска)
init_scheduler()
async def initDB():
from db import DATABASE_URL
from db.initialize import DatabaseInitializer
try:
force = False
reNewDB = False
await DatabaseInitializer(DATABASE_URL).initialize(force, reNewDB)
logger.info("База данных инициализирована")
except Exception as e:
logger.error(f"Инициализация базы завершилась ошибкой: {str(e)}", exc_info=True)
@asynccontextmanager
async def lifespan(app: FastAPI):
# Запускаем lifespan планировщика
@@ -89,13 +76,7 @@ def startServer():
)
async def main():
startServer()
if __name__ == "__main__":
import asyncio
asyncio.run(initDB())
asyncio.run(main())
asyncio.run(startServer())
+18
View File
@@ -0,0 +1,18 @@
async def initDB():
from utils import logger
from db import DATABASE_URL
from db.initialize import DatabaseInitializer
try:
force = False
reNewDB = False
await DatabaseInitializer(DATABASE_URL).initialize(force, reNewDB)
logger.info("База данных инициализирована")
except Exception as e:
logger.error(f"Инициализация базы завершилась ошибкой: {str(e)}", exc_info=True)
if __name__ == "__main__":
import asyncio
asyncio.run(initDB())
+2 -2
View File
@@ -92,7 +92,7 @@ def p7s_save(signature_data, employee_id):
try:
# Базовый путь к директории
base_path = Path("src/attachments/secure")
base_path = Path("attachments/secure").resolve()
# Путь к папке сотрудника
employee_path = base_path / str(employee_id)
@@ -141,7 +141,7 @@ def p7s_delete(employee_id):
from utils import answer
try:
signature_dir = Path(f"src/attachments/secure/{employee_id}")
signature_dir = Path(f"attachments/secure/{employee_id}").resolve()
if signature_dir.exists():
for file in signature_dir.iterdir():
+83 -352
View File
@@ -1,70 +1,47 @@
"""
Модуль для конвертации HTML в PDF с использованием системного WeasyPrint (Homebrew)
Фильтрация пустых строк таблиц, центрирование по text-align: center.
"""
import os
import re
import subprocess
import tempfile
from typing import List, Dict, Tuple
from pathlib import Path
from weasyprint import HTML
from utils import logger
class WeasyPrintConverter:
def __init__(self, output_dir: list[str]):
def __init__(self, output_dir: str):
self.output_dir = Path(output_dir).resolve()
self.output_dir.mkdir(parents=True, exist_ok=True)
self.max_files = 5
self.weasyprint_path = self._find_weasyprint()
def _find_weasyprint(self):
try:
result = subprocess.run(
["which", "weasyprint"], capture_output=True, text=True, check=True
)
weasyprint_path = result.stdout.strip()
logger.info(f"✅ WeasyPrint найден: {weasyprint_path}")
return weasyprint_path
except subprocess.CalledProcessError:
possible_paths = [
"/opt/homebrew/bin/weasyprint",
"/usr/local/bin/weasyprint",
"/usr/bin/weasyprint",
]
for path in possible_paths:
if os.path.exists(path):
logger.info(f"✅ WeasyPrint найден: {path}")
return path
logger.error("❌ WeasyPrint не найден. Установите: brew install weasyprint")
return None
# ---------------------------------------------------------
# SERVICE METHODS
# ---------------------------------------------------------
def _clean_filename(self, filename: str) -> str:
"""Очищает имя файла от недопустимых символов"""
# Только заменяем запрещённые символы на подчёркивание
clean_name = re.sub(r'[<>:"/\\|?*]', "_", filename)
# Убираем пробелы по краям
clean_name = clean_name.strip()
# Ограничиваем длину
if len(clean_name) > 200:
clean_name = clean_name[:200]
# Защита от пустого имени
return clean_name if clean_name else "document"
# ---------------------------------------------------------
# TABLE CLEANING
# ---------------------------------------------------------
def _is_empty_table_row(self, row_html: str) -> bool:
text_only = re.sub(r"<[^>]+>", "", row_html)
clean_text = re.sub(r"[\s\xa0\u200b\u200c\u200d\u2060\ufeff]+", "", text_only)
clean_text = re.sub(r"&[^;]{2,6};", "", clean_text)
has_colspan = "colspan" in row_html.lower()
has_rowspan = "rowspan" in row_html.lower()
has_nbsp = "&nbsp;" in row_html or "&#160;" in row_html
is_not_empty = bool(clean_text) or has_colspan or has_rowspan or has_nbsp
return not is_not_empty
def _remove_empty_table_rows_comprehensive(self, html_content: str) -> str:
"""
Комплексное удаление пустых строк таблиц ВНУТРИ таблиц
Убирает строки без текстового содержимого внутри <table>...</table>
"""
try:
# Находим все таблицы
tables = list(
re.finditer(
r"<table[^>]*>.*?</table>",
@@ -76,60 +53,35 @@ class WeasyPrintConverter:
if not tables:
return html_content
# Обрабатываем с конца, чтобы не сбивать индексы
for table_match in reversed(tables):
table_html = table_match.group(0)
table_start = table_match.start()
table_end = table_match.end()
# Разбираем таблицу на строки
rows = []
pos = 0
table_length = len(table_html)
while pos < table_length:
# Ищем начало строки
tr_start = table_html.lower().find("<tr", pos)
if tr_start == -1:
break
# Ищем конец строки
tr_end = table_html.lower().find("</tr>", tr_start)
if tr_end == -1:
# Нет закрывающего тега - ищем конец таблицы
tr_end = table_html.lower().find("</table>", tr_start)
if tr_end == -1:
tr_end = table_length
tr_end = table_length
tr_end += 5
tr_end += 5 # Длина '</tr>'
row_html = table_html[tr_start:tr_end]
# Проверяем строку на пустоту
is_empty_row = self._is_empty_table_row(row_html)
if not is_empty_row:
# Сохраняем непустую строку
if not self._is_empty_table_row(row_html):
rows.append(row_html)
else:
logger.debug(f"Удалена пустая строка таблицы")
pos = tr_end
if len(rows) == 0:
# Если все строки пустые, удаляем всю таблицу
logger.info(f"🗑️ Удалена полностью пустая таблица")
html_content = html_content[:table_start] + html_content[table_end:]
else:
# Собираем таблицу обратно
new_table_html = table_html
for row in rows:
# Убедимся, что строка уже в таблице
if row not in new_table_html:
# Это не должно происходить, но на всякий случай
pass
# Заменяем оригинальную таблицу (она осталась неизменной если строки не удалялись)
# Но мы все равно пересобираем для чистоты
table_header_end = (
table_html.lower().find(">", table_html.lower().find("<table"))
+ 1
@@ -141,6 +93,7 @@ class WeasyPrintConverter:
+ "".join(rows)
+ table_html[table_footer_start:]
)
html_content = (
html_content[:table_start]
+ new_table
@@ -153,52 +106,24 @@ class WeasyPrintConverter:
logger.warning(f"⚠ Ошибка удаления пустых строк таблиц: {e}")
return html_content
def _is_empty_table_row(self, row_html: str) -> bool:
"""
Проверяет, является ли строка таблицы пустой
"""
# Убираем все HTML теги
text_only = re.sub(r"<[^>]+>", "", row_html)
# Убираем все пробелы, невидимые символы и спецсимволы
clean_text = re.sub(r"[\s\xa0\u200b\u200c\u200d\u2060\ufeff]+", "", text_only)
# Убираем HTML entities (но оставляем &nbsp; как признак непустоты)
clean_text = re.sub(r"&[^;]{2,6};", "", clean_text)
# Проверяем специальные случаи
has_colspan = "colspan" in row_html.lower()
has_rowspan = "rowspan" in row_html.lower()
has_nbsp = "&nbsp;" in row_html or "&#160;" in row_html
# Строка считается НЕ пустой если:
# 1. Есть текст после очистки
# 2. Есть объединенные ячейки
# 3. Есть неразрывные пробелы
is_not_empty = bool(clean_text) or has_colspan or has_rowspan or has_nbsp
return not is_not_empty
# ---------------------------------------------------------
# ALIGNMENT FIX
# ---------------------------------------------------------
def _center_elements_with_center_alignment(self, html_content: str) -> str:
"""
Находит элементы с text-align: center и гарантирует их центрирование
"""
try:
# Ищем элементы со стилем text-align: center
def add_center_alignment(match):
full_match = match.group(0)
element_with_style = match.group(1)
element_name = match.group(2)
style_content = match.group(3)
# Если уже есть align="center", оставляем как есть
if (
'align="center"' in element_with_style
or "align='center'" in element_with_style
):
return full_match
# Добавляем align="center" к элементу
if element_name in [
"p",
"div",
@@ -211,289 +136,103 @@ class WeasyPrintConverter:
"td",
"th",
]:
new_element = element_with_style.replace(
return element_with_style.replace(
f"<{element_name}", f'<{element_name} align="center"'
)
return new_element + match.group(4)
) + match.group(4)
return full_match
# Паттерн для поиска элементов с text-align: center
pattern = r'(<(\w+)[^>]*style\s*=\s*["\'][^"\']*text-align\s*:\s*center[^"\']*["\'][^>]*>)(.*?)'
html_content = re.sub(
return re.sub(
pattern,
add_center_alignment,
html_content,
flags=re.IGNORECASE | re.DOTALL,
)
except Exception:
return html_content
except Exception as e:
# logger.warning(f"⚠ Ошибка центрирования элементов: {e}")
return html_content
# ---------------------------------------------------------
# HTML PREP
# ---------------------------------------------------------
def _prepare_html(self, html_content: str) -> str:
"""Подготавливает HTML для конвертации"""
try:
# 1. Удаляем изображения
html_content = re.sub(r"<img[^>]*>", "", html_content, flags=re.IGNORECASE)
# 2. Убираем ВСЕ пустые строки таблиц (основная задача)
html_content = self._remove_empty_table_rows_comprehensive(html_content)
# 3. Центрируем элементы с text-align: center
html_content = self._center_elements_with_center_alignment(html_content)
# 4. Добавляем базовую структуру если её нет
if not html_content.strip().startswith("<!DOCTYPE"):
html_content = f"""<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
/* МИНИМАЛЬНЫЕ СТИЛИ - почти не вмешиваемся в оригинальный формат */
@page {{
size: A4;
margin: 0.5cm !important;
}}
/* Основные настройки */
body {{
font-family: "DejaVu Sans", Arial, sans-serif;
font-size: 9pt !important;
line-height: 1.0 !important;
color: #000000;
}}
/* ТАБЛИЦЫ - сохраняем оригинальный формат! */
table {{
border-collapse: collapse !important;
margin: 0 !important; /* Убрали все отступы вокруг таблиц */
font-size: 8.5pt !important;
page-break-inside: auto !important;
}}
/* ЯЧЕЙКИ ТАБЛИЦ - только вертикальное центрирование и небольшие отступы */
th, td {{
padding: 3px 5px !important; /* Минимальные отступы */
vertical-align: middle !important; /* Вертикальное центрирование */
}}
/* НЕ добавляем границы автоматически! */
/* Границы будут ТОЛЬКО там, где они были в оригинале */
/* Горизонтальное центрирование заголовков таблиц */
th {{
text-align: center !important;
}}
/* Элементы с align="center" */
[align="center"] {{
text-align: center !important;
}}
/* Элементы со style*="text-align: center" */
[style*="text-align: center"] {{
text-align: center !important;
}}
/* Заголовки документа */
h1, h2, h3, h4, h5, h6 {{
font-weight: bold !important;
margin-top: 8px !important;
margin-bottom: 4px !important;
}}
h1 {{ font-size: 11pt !important; page-break-before: always !important; }}
h2 {{ font-size: 10pt !important; }}
h3 {{ font-size: 9.5pt !important; }}
/* Параграфы - минимальные отступы */
p {{
margin: 0 0 3px 0 !important;
}}
/* Форматирование текста */
strong, b {{
font-weight: bold !important;
}}
em, i {{
font-style: italic !important;
}}
/* Списки */
ul, ol {{
margin: 3px 0 3px 15px !important;
padding: 0 !important;
}}
li {{
margin-bottom: 1px !important;
}}
/* Убираем лишние отступы */
div, p, span {{
margin: 0 !important;
padding: 0 !important;
}}
br {{
line-height: 0.5 !important;
margin: 0 !important;
padding: 0 !important;
}}
/* Убираем отступы перед таблицами на уровне CSS */
:before(table) {{
margin-bottom: 0 !important;
padding-bottom: 0 !important;
}}
</style>
<meta charset="UTF-8">
<style>
@page {{ size: A4; margin: 0.5cm; }}
body {{
font-family: "DejaVu Sans", Arial, sans-serif;
font-size: 9pt;
line-height: 1.0;
}}
table {{
border-collapse: collapse;
margin: 0;
font-size: 8.5pt;
}}
th, td {{
padding: 3px 5px;
vertical-align: middle;
}}
th {{
text-align: center;
}}
</style>
</head>
<body>
{html_content}
</body>
</html>"""
# 5. Убеждаемся в наличии charset
if "<meta charset=" not in html_content.lower():
html_content = html_content.replace(
"<head>", '<head><meta charset="UTF-8">', 1
)
# 6. ОБРАБОТКА ТАБЛИЦ:
# Только добавляем вертикальное центрирование и отступы, НЕ меняем границы!
# Добавляем vertical-align: middle ко всем ячейкам если его нет
def add_vertical_align(match):
cell_tag = match.group(0)
if (
"valign=" not in cell_tag.lower()
and "vertical-align:" not in cell_tag.lower()
):
return cell_tag.replace("<td", '<td valign="middle"').replace(
"<th", '<th valign="middle"'
)
return cell_tag
html_content = re.sub(
r"<(td|th)(?![^>]*(?:valign|vertical-align))[^>]*>",
add_vertical_align,
html_content,
flags=re.IGNORECASE,
)
# Добавляем text-align: center к заголовкам таблиц если нет своего выравнивания
def add_th_center(match):
th_tag = match.group(0)
if (
"align=" not in th_tag.lower()
and "text-align:" not in th_tag.lower()
):
return th_tag.replace("<th", '<th align="center"')
return th_tag
html_content = re.sub(
r"<th[^>]*>", add_th_center, html_content, flags=re.IGNORECASE
)
# 7. ФИНАЛЬНАЯ ПРОВЕРКА: еще раз удаляем пустые строки таблиц после всех модификаций
html_content = self._remove_empty_table_rows_comprehensive(html_content)
logger.debug(f"HTML подготовлен, размер: {len(html_content)} символов")
return html_content
except Exception as e:
logger.error(f"❌ Ошибка подготовки HTML: {e}")
# Минимальная обработка в случае ошибки
return f"""<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
@page {{ margin: 0.5cm; }}
body {{ font-size: 9pt; line-height: 1.0; }}
table {{ border-collapse: collapse; margin: 0; }}
th, td {{ padding: 3px 5px; vertical-align: middle; }}
th {{ text-align: center; }}
</style>
</head>
<body>{html_content}</body>
</html>"""
return html_content
# ---------------------------------------------------------
# MAIN CONVERSION
# ---------------------------------------------------------
def convert_html_to_pdf(
self, html_content: str, output_path: Path
) -> Tuple[bool, str]:
"""
Конвертирует HTML в PDF и сохраняет во все указанные папки
Оптимизированная версия: конвертирует один раз, копирует результат
"""
if not self.weasyprint_path:
return False, "WeasyPrint не найден. Установите: brew install weasyprint"
try:
prepared_html = self._prepare_html(html_content)
with tempfile.NamedTemporaryFile(
mode="w", suffix=".html", delete=False, encoding="utf-8"
) as f:
f.write(prepared_html)
temp_html = f.name
HTML(string=prepared_html, base_url=str(self.output_dir)).write_pdf(
target=str(output_path), presentational_hints=True
)
try:
cmd = [
self.weasyprint_path,
temp_html,
output_path,
"--encoding",
"utf-8",
"--presentational-hints",
]
if not output_path.exists() or output_path.stat().st_size < 1024:
return False, "Созданный PDF слишком мал или отсутствует"
result = subprocess.run(
cmd, capture_output=True, text=True, timeout=180
)
return True, f"✅ PDF создан: {output_path}"
if result.returncode != 0:
error_msg = (
result.stderr[:300] if result.stderr else "Неизвестная ошибка"
)
return False, f"Ошибка конвертации: {error_msg}"
if not output_path.exists() or output_path.stat().st_size < 1024:
return False, "Созданный PDF слишком мал или отсутствует"
# Формируем результат
return (
True,
f"✅ PDF создан и сохранен: {output_path}",
)
finally:
if os.path.exists(temp_html):
os.unlink(temp_html)
except subprocess.TimeoutExpired:
return False, "Таймаут при конвертации (180 секунд)"
except Exception as e:
logger.error(f"Неожиданная ошибка: {str(e)}")
return False, f"Ошибка конвертации: {str(e)}"
logger.error(f"Ошибка конвертации: {e}")
return False, str(e)
# ---------------------------------------------------------
# BATCH PROCESSING
# ---------------------------------------------------------
def convert_documents(self, docs: List[Dict]) -> Dict[str, Dict]:
results = {
"status": "SUCCESS",
"files": [],
}
results = {"status": "SUCCESS", "files": []}
if len(docs) > self.max_files:
logger.warning(
f"⚠ Получено {len(docs)} документов, обработано {self.max_files}"
)
docs = docs[: self.max_files]
for i, doc in enumerate(docs, 1):
@@ -513,34 +252,26 @@ class WeasyPrintConverter:
}
)
logger.info(f"📄 Документ {i}/{len(docs)}: {filename}")
if not html_content or len(html_content.strip()) < 100:
results["status"] = "ERROR"
logger.warning(f" ⚠ Пропущен: недостаточно контента")
continue
success, message = self.convert_html_to_pdf(html_content, output_path)
if success:
logger.info(f" ✅ Успешно")
else:
logger.error(f" ❌ Ошибка: {message}")
if not success:
results["status"] = "ERROR"
logger.error(message)
return results
def convert_docs_to_pdfs(docs: List[Dict], idPatientMis: str) -> Dict:
try:
baseDir = "src/attachments/export/"
outputDir = f"{baseDir}{idPatientMis}"
converter = WeasyPrintConverter(outputDir)
base_dir = "attachments/export/"
output_dir = f"{base_dir}{idPatientMis}"
converter = WeasyPrintConverter(output_dir)
return converter.convert_documents(docs)
except Exception as e:
logger.error(f"❌ Критическая ошибка в convert_docs_to_pdfs: {str(e)}")
return {
"status": "ERROR",
"message": f"Критическая ошибка в convert_docs_to_pdfs: {str(e)}",
}
logger.error(f"❌ Критическая ошибка: {e}")
return {"status": "ERROR", "message": str(e)}
-1
View File
@@ -22,7 +22,6 @@ async def render(
context = {
"request": request,
"content": {},
# "content": {"app_secret": config.APP_SECRET},
}
fileName = f"{request.scope['path']}/index.html"