комментарии на инструментах
This commit is contained in:
Binary file not shown.
Binary file not shown.
@@ -82,6 +82,24 @@ async def toolkit_request(
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/comment", summary="Добавление комментария")
|
||||||
|
async def add_comment(reqData: dict = Depends(requestDict)):
|
||||||
|
logger.info(f"Добавление комментария")
|
||||||
|
response = {"status": "error"}
|
||||||
|
try:
|
||||||
|
logger.info(f"Добавление комментария")
|
||||||
|
logger.info(reqData.get("body"))
|
||||||
|
toolkitId = int(reqData.get("body").get("toolkitId"))
|
||||||
|
comment = reqData.get("body").get("commentText")
|
||||||
|
userId = int(reqData.get("body").get("userId"))
|
||||||
|
result = await ToolkitHandler.addComment(toolkitId, userId, comment)
|
||||||
|
response = handleResult(result, response)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
finally:
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
@router.post("/fill_prepare", summary="Подготовка заполнения ящика")
|
@router.post("/fill_prepare", summary="Подготовка заполнения ящика")
|
||||||
async def fill_toolbox():
|
async def fill_toolbox():
|
||||||
logger.info(f"Подготовка заполнения ящика")
|
logger.info(f"Подготовка заполнения ящика")
|
||||||
|
|||||||
@@ -3823,6 +3823,7 @@ async function getToolkitStocks(toolkitId) {
|
|||||||
return resp.data;
|
return resp.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Функция показа модального окна с деталями инструмента
|
||||||
// Функция показа модального окна с деталями инструмента
|
// Функция показа модального окна с деталями инструмента
|
||||||
async function showToolkitDetailsModal(toolkitId) {
|
async function showToolkitDetailsModal(toolkitId) {
|
||||||
const modalId = 'toolkitDetailsModal';
|
const modalId = 'toolkitDetailsModal';
|
||||||
@@ -3920,6 +3921,36 @@ async function showToolkitDetailsModal(toolkitId) {
|
|||||||
let toolkitStocksData = null;
|
let toolkitStocksData = null;
|
||||||
let isStocksLoading = false;
|
let isStocksLoading = false;
|
||||||
|
|
||||||
|
// Форматирование даты комментария
|
||||||
|
let commentDateInfo = '';
|
||||||
|
if (toolkitData.comment_at) {
|
||||||
|
const commentDate = new Date(toolkitData.comment_at);
|
||||||
|
commentDateInfo = `
|
||||||
|
<div class="text-muted small">
|
||||||
|
<i class="bi bi-clock me-1"></i>
|
||||||
|
Последнее изменение: ${commentDate.toLocaleDateString('ru-RU')} ${commentDate.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' })}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
commentDateInfo = `
|
||||||
|
<div class="text-muted small">
|
||||||
|
<i class="bi bi-info-circle me-1"></i>
|
||||||
|
Комментарии еще не оставляли
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Информация о пользователе, оставившем комментарий
|
||||||
|
let commentUserInfo = '';
|
||||||
|
if (toolkitData.comment_user_data && toolkitData.comment_user_data.username) {
|
||||||
|
commentUserInfo = `
|
||||||
|
<div class="text-muted small">
|
||||||
|
<i class="bi bi-person me-1"></i>
|
||||||
|
Автор: ${toolkitData.comment_user_data.username}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
modal.innerHTML = `
|
modal.innerHTML = `
|
||||||
<div class="modal-dialog modal-lg">
|
<div class="modal-dialog modal-lg">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
@@ -3993,6 +4024,27 @@ async function showToolkitDetailsModal(toolkitId) {
|
|||||||
` : ''}
|
` : ''}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Секция комментариев -->
|
||||||
|
<div class="border-top pt-3 mt-3">
|
||||||
|
<h6><i class="bi bi-chat-left-text me-1"></i>Комментарий</h6>
|
||||||
|
<div class="mb-2">
|
||||||
|
${commentDateInfo}
|
||||||
|
${commentUserInfo}
|
||||||
|
</div>
|
||||||
|
<div class="mb-2">
|
||||||
|
<textarea class="form-control" id="toolkitComment" rows="3"
|
||||||
|
placeholder="Введите комментарий...">${toolkitData.comment_text || ''}</textarea>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<div class="form-text">
|
||||||
|
Комментарий будет сохранен для этого инструмента
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn btn-primary btn-sm" id="saveCommentBtn" disabled>
|
||||||
|
<i class="bi bi-save me-1"></i>Сохранить
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Закрыть</button>
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Закрыть</button>
|
||||||
@@ -4100,6 +4152,101 @@ async function showToolkitDetailsModal(toolkitId) {
|
|||||||
const bsModal = new bootstrap.Modal(modal);
|
const bsModal = new bootstrap.Modal(modal);
|
||||||
bsModal.show();
|
bsModal.show();
|
||||||
|
|
||||||
|
// Элементы для работы с комментарием
|
||||||
|
const commentTextarea = modal.querySelector('#toolkitComment');
|
||||||
|
const saveCommentBtn = modal.querySelector('#saveCommentBtn');
|
||||||
|
const originalComment = toolkitData.comment_text || '';
|
||||||
|
|
||||||
|
// Проверка изменения комментария
|
||||||
|
const checkCommentChanged = () => {
|
||||||
|
const currentComment = commentTextarea.value.trim();
|
||||||
|
const isChanged = currentComment !== originalComment;
|
||||||
|
saveCommentBtn.disabled = !isChanged || currentComment === '';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Слушатель изменений в текстовом поле
|
||||||
|
commentTextarea.addEventListener('input', checkCommentChanged);
|
||||||
|
|
||||||
|
// Обработчик сохранения комментария
|
||||||
|
saveCommentBtn.addEventListener('click', async () => {
|
||||||
|
const commentText = commentTextarea.value.trim();
|
||||||
|
|
||||||
|
if (!commentText) {
|
||||||
|
showInfo('Комментарий не может быть пустым', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Блокируем кнопку на время отправки
|
||||||
|
saveCommentBtn.disabled = true;
|
||||||
|
saveCommentBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span>Сохранение...';
|
||||||
|
|
||||||
|
// Отправляем комментарий на сервер
|
||||||
|
const response = await apiRequest('/toolkit/comment', {
|
||||||
|
toolkitId: toolkitData.id,
|
||||||
|
userId: userData.id,
|
||||||
|
commentText: commentText
|
||||||
|
}, 'POST');
|
||||||
|
|
||||||
|
if (response.status === 'ok') {
|
||||||
|
// Обновляем информацию о комментарии без перезагрузки страницы
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
// Обновляем информацию о дате
|
||||||
|
const dateInfoDiv = modal.querySelector('.border-top .text-muted.small:first-child');
|
||||||
|
if (dateInfoDiv) {
|
||||||
|
dateInfoDiv.innerHTML = `
|
||||||
|
<i class="bi bi-clock me-1"></i>
|
||||||
|
Последнее изменение: ${now.toLocaleDateString('ru-RU')} ${now.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' })}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обновляем информацию о пользователе
|
||||||
|
const userInfoDiv = modal.querySelector('.border-top .text-muted.small:nth-child(2)');
|
||||||
|
if (userInfoDiv) {
|
||||||
|
userInfoDiv.innerHTML = `
|
||||||
|
<i class="bi bi-person me-1"></i>
|
||||||
|
Автор: ${userData.username || 'Текущий пользователь'}
|
||||||
|
`;
|
||||||
|
} else if (userData.username) {
|
||||||
|
// Если элемента с информацией о пользователе не было, создаем его
|
||||||
|
const commentInfoDiv = modal.querySelector('.border-top .mb-2');
|
||||||
|
if (commentInfoDiv) {
|
||||||
|
const newUserInfo = document.createElement('div');
|
||||||
|
newUserInfo.className = 'text-muted small';
|
||||||
|
newUserInfo.innerHTML = `
|
||||||
|
<i class="bi bi-person me-1"></i>
|
||||||
|
Автор: ${userData.username}
|
||||||
|
`;
|
||||||
|
commentInfoDiv.appendChild(newUserInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обновляем оригинальный комментарий для дальнейших проверок
|
||||||
|
toolkitData.comment_text = commentText;
|
||||||
|
toolkitData.comment_at = now.toISOString();
|
||||||
|
toolkitData.comment_user_data = { username: userData.username };
|
||||||
|
|
||||||
|
// Показываем успешное сообщение
|
||||||
|
showInfo('Комментарий успешно сохранен', 'success');
|
||||||
|
|
||||||
|
// Кнопка становится неактивной, так как изменения сохранены
|
||||||
|
saveCommentBtn.innerHTML = '<i class="bi bi-save me-1"></i>Сохранено';
|
||||||
|
setTimeout(() => {
|
||||||
|
saveCommentBtn.innerHTML = '<i class="bi bi-save me-1"></i>Сохранить';
|
||||||
|
checkCommentChanged();
|
||||||
|
}, 1500);
|
||||||
|
} else {
|
||||||
|
throw new Error(response.message || 'Ошибка сохранения комментария');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при сохранении комментария:', error);
|
||||||
|
showInfo(error.message || 'Произошла ошибка при сохранении комментария', 'danger');
|
||||||
|
saveCommentBtn.disabled = false;
|
||||||
|
saveCommentBtn.innerHTML = '<i class="bi bi-save me-1"></i>Сохранить';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Функция для загрузки данных об остатках
|
// Функция для загрузки данных об остатках
|
||||||
const loadToolkitStocks = async () => {
|
const loadToolkitStocks = async () => {
|
||||||
if (isStocksLoading) return;
|
if (isStocksLoading) return;
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
+26
-1
@@ -1,5 +1,6 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from db.handlers.stock import StockHandler
|
from db.handlers.stock import StockHandler
|
||||||
|
from db.handlers.user import UserHandler
|
||||||
from utils import logger, saveImage, safeFilename
|
from utils import logger, saveImage, safeFilename
|
||||||
from db import CRUD
|
from db import CRUD
|
||||||
from db.schemas.toolkit import Toolkit
|
from db.schemas.toolkit import Toolkit
|
||||||
@@ -175,7 +176,12 @@ class ToolkitHandler:
|
|||||||
if not toolkit:
|
if not toolkit:
|
||||||
logger.error("Инструмент не найден")
|
logger.error("Инструмент не найден")
|
||||||
return {}
|
return {}
|
||||||
return toolkit.toDict()
|
if toolkit.comment_user_id:
|
||||||
|
user_data = await UserHandler.get(toolkit.comment_user_id)
|
||||||
|
data = toolkit.toDict()
|
||||||
|
data["comment_user_data"] = user_data
|
||||||
|
logger.info(data)
|
||||||
|
return data
|
||||||
|
|
||||||
async def getSeveral(toolkitIds: list[int]) -> list[dict]:
|
async def getSeveral(toolkitIds: list[int]) -> list[dict]:
|
||||||
query = select(Toolkit).where(Toolkit.id.in_(toolkitIds))
|
query = select(Toolkit).where(Toolkit.id.in_(toolkitIds))
|
||||||
@@ -211,6 +217,25 @@ class ToolkitHandler:
|
|||||||
)
|
)
|
||||||
return {"status": "ok"} if result else {"errorMessage": "Инструмент не удален"}
|
return {"status": "ok"} if result else {"errorMessage": "Инструмент не удален"}
|
||||||
|
|
||||||
|
async def addComment(toolkitId: int, user_id: int, comment: str):
|
||||||
|
logger.info(f"Добавление комментария к инструменту {toolkitId}...")
|
||||||
|
logger.info(f"Комментарий: {comment}")
|
||||||
|
logger.info(f"Пользователь: {user_id}")
|
||||||
|
query = select(Toolkit).where(Toolkit.id == toolkitId)
|
||||||
|
toolkit = await CRUD.read(query)
|
||||||
|
if not toolkit:
|
||||||
|
logger.error("Инструмент не найден")
|
||||||
|
return {"errorMessage": "Инструмент не найден"}
|
||||||
|
try:
|
||||||
|
await toolkit.edit(
|
||||||
|
comment_text=comment, comment_user_id=user_id, comment_at=datetime.now()
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка добавления комментария: {str(e)}")
|
||||||
|
return {"errorMessage": f"Ошибка добавления комментария: {str(e)}"}
|
||||||
|
logger.info(f"Комментарий к инструменту {toolkit.title} успешно добавлен")
|
||||||
|
return {"status": "ok"}
|
||||||
|
|
||||||
async def initialize():
|
async def initialize():
|
||||||
from .categories import CategoryHandler
|
from .categories import CategoryHandler
|
||||||
|
|
||||||
|
|||||||
+140
-57
@@ -1,3 +1,12 @@
|
|||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from sqlalchemy import inspect, text
|
||||||
|
from sqlalchemy.ext.asyncio import create_async_engine, AsyncEngine
|
||||||
|
from sqlalchemy.schema import CreateTable
|
||||||
|
from sqlalchemy.dialects.postgresql import dialect as pg_dialect
|
||||||
|
|
||||||
|
from utils import logger
|
||||||
|
|
||||||
from db.handlers.access import AccessLevelHandler
|
from db.handlers.access import AccessLevelHandler
|
||||||
from db.handlers.user import UserHandler
|
from db.handlers.user import UserHandler
|
||||||
from db.handlers.toolbox import ToolboxHandler
|
from db.handlers.toolbox import ToolboxHandler
|
||||||
@@ -5,12 +14,6 @@ from db.handlers.categories import CategoryHandler
|
|||||||
from db.handlers.toolkit import ToolkitHandler
|
from db.handlers.toolkit import ToolkitHandler
|
||||||
from db.handlers.actions import StocksActions
|
from db.handlers.actions import StocksActions
|
||||||
|
|
||||||
from typing import Optional
|
|
||||||
from sqlalchemy import inspect, text
|
|
||||||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncEngine
|
|
||||||
from sqlalchemy.schema import CreateTable
|
|
||||||
from utils import logger
|
|
||||||
|
|
||||||
|
|
||||||
class DatabaseInitializer:
|
class DatabaseInitializer:
|
||||||
existing_tables: Optional[list[str]] = None
|
existing_tables: Optional[list[str]] = None
|
||||||
@@ -22,6 +25,10 @@ class DatabaseInitializer:
|
|||||||
self.engine: Optional[AsyncEngine] = None
|
self.engine: Optional[AsyncEngine] = None
|
||||||
self.metadata = Base.metadata
|
self.metadata = Base.metadata
|
||||||
|
|
||||||
|
# ==========================================================
|
||||||
|
# PUBLIC
|
||||||
|
# ==========================================================
|
||||||
|
|
||||||
async def initialize(self, force: bool = False, reNewDB: bool = False):
|
async def initialize(self, force: bool = False, reNewDB: bool = False):
|
||||||
"""Main database initialization method"""
|
"""Main database initialization method"""
|
||||||
try:
|
try:
|
||||||
@@ -29,32 +36,39 @@ class DatabaseInitializer:
|
|||||||
|
|
||||||
async with self.engine.begin() as conn:
|
async with self.engine.begin() as conn:
|
||||||
if force:
|
if force:
|
||||||
logger.info("Принудительное удаление и создание баз...")
|
logger.warning("Принудительное удаление и создание БД...")
|
||||||
await self._drop_all()
|
await self._drop_all()
|
||||||
await self._create_tables_directly()
|
await self._create_tables_directly()
|
||||||
await self._initialize_data(reNewDB)
|
await self._initialize_data(reNewDB)
|
||||||
elif not await self._check_tables_exist(conn):
|
return
|
||||||
logger.info("Не все необходимые таблицы существуют. Создаем...")
|
|
||||||
|
tables_exist = await self._check_tables_exist(conn)
|
||||||
|
|
||||||
|
if not tables_exist:
|
||||||
|
logger.warning("Не все таблицы существуют. Создаем недостающие...")
|
||||||
await self._create_tables_directly()
|
await self._create_tables_directly()
|
||||||
|
|
||||||
# Проверяем после создания
|
# 🔥 СИНХРОНИЗАЦИЯ СХЕМЫ
|
||||||
async with self.engine.begin() as conn:
|
logger.info("Синхронизация схемы БД...")
|
||||||
if not await self._check_tables_exist(conn):
|
await self._sync_schema(conn)
|
||||||
raise RuntimeError("Не все необходимые таблицы существуют!")
|
|
||||||
|
|
||||||
if reNewDB:
|
if reNewDB:
|
||||||
logger.info("Принудительная загрузка данных...")
|
logger.warning("Принудительная загрузка данных...")
|
||||||
await self._initialize_data(reNewDB)
|
await self._initialize_data(reNewDB)
|
||||||
else:
|
|
||||||
logger.info("Все необходимые таблицы существуют. Пропускаем...")
|
logger.info("Инициализация БД завершена успешно")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Инициализация базы завершилась ошибкой: {str(e)}")
|
logger.exception("Инициализация базы завершилась ошибкой")
|
||||||
raise
|
raise
|
||||||
finally:
|
finally:
|
||||||
if self.engine:
|
if self.engine:
|
||||||
await self.engine.dispose()
|
await self.engine.dispose()
|
||||||
|
|
||||||
|
# ==========================================================
|
||||||
|
# CHECK
|
||||||
|
# ==========================================================
|
||||||
|
|
||||||
async def _check_tables_exist(self, conn) -> bool:
|
async def _check_tables_exist(self, conn) -> bool:
|
||||||
"""Check if all tables from metadata exist"""
|
"""Check if all tables from metadata exist"""
|
||||||
try:
|
try:
|
||||||
@@ -70,12 +84,8 @@ class DatabaseInitializer:
|
|||||||
|
|
||||||
missing_tables = required_tables - set(DatabaseInitializer.existing_tables)
|
missing_tables = required_tables - set(DatabaseInitializer.existing_tables)
|
||||||
if missing_tables:
|
if missing_tables:
|
||||||
logger.warning("Существующие таблицы:")
|
|
||||||
logger.info(DatabaseInitializer.existing_tables)
|
|
||||||
logger.warning("Необходимые таблицы:")
|
|
||||||
logger.info(required_tables)
|
|
||||||
logger.warning("Отсутствующие таблицы:")
|
logger.warning("Отсутствующие таблицы:")
|
||||||
logger.info(missing_tables)
|
logger.warning(missing_tables)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
@@ -83,62 +93,135 @@ class DatabaseInitializer:
|
|||||||
logger.warning(f"Проверка таблиц завершилась ошибкой: {str(e)}")
|
logger.warning(f"Проверка таблиц завершилась ошибкой: {str(e)}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# ==========================================================
|
||||||
|
# CREATE / DROP
|
||||||
|
# ==========================================================
|
||||||
|
|
||||||
async def _create_tables_directly(self):
|
async def _create_tables_directly(self):
|
||||||
"""Create tables directly using SQLAlchemy (bypass Alembic)"""
|
"""Create tables directly using SQLAlchemy (bypass Alembic)"""
|
||||||
async with self.engine.begin() as conn:
|
async with self.engine.begin() as conn:
|
||||||
# Создаем все таблицы из метаданных
|
|
||||||
for table in self.metadata.sorted_tables:
|
for table in self.metadata.sorted_tables:
|
||||||
try:
|
if (
|
||||||
if (
|
DatabaseInitializer.existing_tables
|
||||||
DatabaseInitializer.existing_tables
|
and table.name in DatabaseInitializer.existing_tables
|
||||||
and table.name in DatabaseInitializer.existing_tables
|
):
|
||||||
):
|
logger.debug(f"Таблица {table.name} уже существует")
|
||||||
logger.debug(
|
continue
|
||||||
f"Таблица {table.name} уже существует. Пропускаем..."
|
|
||||||
)
|
logger.info(f"Создаем таблицу: {table.name}")
|
||||||
continue
|
await conn.execute(CreateTable(table))
|
||||||
create_stmt = CreateTable(table)
|
|
||||||
logger.info(f"Создаем таблицу: {table.name}")
|
|
||||||
await conn.execute(create_stmt)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Ошибка создания таблицы: {str(e)}")
|
|
||||||
logger.warning("Все таблицы успешно созданы")
|
|
||||||
|
|
||||||
async def _drop_all(self):
|
async def _drop_all(self):
|
||||||
"""Drop all tables"""
|
"""Drop all tables"""
|
||||||
async with self.engine.begin() as conn:
|
async with self.engine.begin() as conn:
|
||||||
# Получаем список всех таблиц
|
|
||||||
existing_tables = await conn.run_sync(
|
existing_tables = await conn.run_sync(
|
||||||
lambda sync_conn: inspect(sync_conn).get_table_names()
|
lambda sync_conn: inspect(sync_conn).get_table_names()
|
||||||
)
|
)
|
||||||
|
|
||||||
for table in existing_tables:
|
for table in existing_tables:
|
||||||
drop_stmt = text(
|
logger.warning(f"Удаляем таблицу: {table}")
|
||||||
f'DROP TABLE "{table}" CASCADE'
|
await conn.execute(text(f'DROP TABLE "{table}" CASCADE'))
|
||||||
) # Кавычки на случай спец. символов
|
|
||||||
logger.info(f"Удаляем таблицу: {table}")
|
|
||||||
await conn.execute(drop_stmt)
|
|
||||||
|
|
||||||
logger.warning("Все таблицы успешно удалены")
|
# ==========================================================
|
||||||
|
# SCHEMA SYNC
|
||||||
|
# ==========================================================
|
||||||
|
|
||||||
|
async def _sync_schema(self, conn):
|
||||||
|
await self._add_missing_columns(conn)
|
||||||
|
await self._add_missing_foreign_keys(conn)
|
||||||
|
|
||||||
|
async def _add_missing_columns(self, conn):
|
||||||
|
for table_name, table in self.metadata.tables.items():
|
||||||
|
|
||||||
|
db_columns = await conn.run_sync(
|
||||||
|
lambda sync_conn: {
|
||||||
|
col["name"] for col in inspect(sync_conn).get_columns(table_name)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
for column in table.columns:
|
||||||
|
if column.name in db_columns:
|
||||||
|
continue
|
||||||
|
|
||||||
|
ddl = (
|
||||||
|
f"ALTER TABLE {table_name} "
|
||||||
|
f"ADD COLUMN {self._compile_column(column)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.warning(f"[ADD COLUMN] {table_name}.{column.name}")
|
||||||
|
await conn.execute(text(ddl))
|
||||||
|
|
||||||
|
async def _add_missing_foreign_keys(self, conn):
|
||||||
|
for table_name, table in self.metadata.tables.items():
|
||||||
|
|
||||||
|
db_fks = await conn.run_sync(
|
||||||
|
lambda sync_conn: inspect(sync_conn).get_foreign_keys(table_name)
|
||||||
|
)
|
||||||
|
|
||||||
|
db_fk_pairs = {
|
||||||
|
(
|
||||||
|
tuple(fk["constrained_columns"]),
|
||||||
|
fk["referred_table"],
|
||||||
|
)
|
||||||
|
for fk in db_fks
|
||||||
|
}
|
||||||
|
|
||||||
|
for fk in table.foreign_keys:
|
||||||
|
col = fk.parent.name
|
||||||
|
ref_table = fk.column.table.name
|
||||||
|
ref_col = fk.column.name
|
||||||
|
|
||||||
|
key = ((col,), ref_table)
|
||||||
|
if key in db_fk_pairs:
|
||||||
|
continue
|
||||||
|
|
||||||
|
constraint_name = f"fk_{table_name}_{col}"
|
||||||
|
|
||||||
|
ddl = f"""
|
||||||
|
ALTER TABLE {table_name}
|
||||||
|
ADD CONSTRAINT {constraint_name}
|
||||||
|
FOREIGN KEY ({col})
|
||||||
|
REFERENCES {ref_table}({ref_col})
|
||||||
|
"""
|
||||||
|
|
||||||
|
if fk.ondelete:
|
||||||
|
ddl += f" ON DELETE {fk.ondelete}"
|
||||||
|
|
||||||
|
logger.warning(f"[ADD FK] {table_name}.{col} → {ref_table}.{ref_col}")
|
||||||
|
await conn.execute(text(ddl))
|
||||||
|
|
||||||
|
# ==========================================================
|
||||||
|
# HELPERS
|
||||||
|
# ==========================================================
|
||||||
|
|
||||||
|
def _compile_column(self, column) -> str:
|
||||||
|
"""Compile SQLAlchemy Column to SQL"""
|
||||||
|
ddl = f"{column.name} {column.type.compile(dialect=pg_dialect())}"
|
||||||
|
|
||||||
|
if not column.nullable:
|
||||||
|
ddl += " NULL" # безопасно, NOT NULL позже вручную
|
||||||
|
|
||||||
|
if column.server_default is not None:
|
||||||
|
ddl += f" DEFAULT {column.server_default.arg}"
|
||||||
|
|
||||||
|
return ddl
|
||||||
|
|
||||||
|
# ==========================================================
|
||||||
|
# DATA INIT
|
||||||
|
# ==========================================================
|
||||||
|
|
||||||
async def _initialize_data(self, waiting: bool = False):
|
async def _initialize_data(self, waiting: bool = False):
|
||||||
"""Initialize required data"""
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
logger.info("Инициализация данных...")
|
logger.info("Инициализация данных...")
|
||||||
logger.warning("Инициализация Прав доступа...")
|
|
||||||
await AccessLevelHandler.initialize()
|
await AccessLevelHandler.initialize()
|
||||||
logger.warning("Инициализация Пользователей...")
|
|
||||||
await UserHandler.initialize()
|
await UserHandler.initialize()
|
||||||
logger.warning("Инициализация Туллбоксов...")
|
|
||||||
await ToolboxHandler.initialize()
|
await ToolboxHandler.initialize()
|
||||||
logger.warning("Инициализация Категорий...")
|
|
||||||
await CategoryHandler.initialize()
|
await CategoryHandler.initialize()
|
||||||
logger.warning("Инициализация Инструментов...")
|
|
||||||
await ToolkitHandler.initialize()
|
await ToolkitHandler.initialize()
|
||||||
logger.warning("Инициализация Складов...")
|
|
||||||
await StocksActions.initialize()
|
await StocksActions.initialize()
|
||||||
|
|
||||||
logger.info("Данные успешно инициализированы")
|
logger.info("Данные успешно инициализированы")
|
||||||
except Exception as e:
|
except Exception:
|
||||||
logger.error(f"Инициализация данных завершилась ошибкой: {str(e)}")
|
logger.exception("Ошибка инициализации данных")
|
||||||
raise
|
raise
|
||||||
|
|||||||
Binary file not shown.
@@ -14,18 +14,16 @@ class Toolkit(Base):
|
|||||||
description = Column(Text, nullable=True)
|
description = Column(Text, nullable=True)
|
||||||
specifications = Column(JSONB, default={})
|
specifications = Column(JSONB, default={})
|
||||||
category_id = Column(Integer, ForeignKey("categories.id", ondelete="CASCADE"))
|
category_id = Column(Integer, ForeignKey("categories.id", ondelete="CASCADE"))
|
||||||
category_data = relationship(
|
|
||||||
"Category",
|
|
||||||
cascade="all, delete-orphan",
|
|
||||||
lazy="joined",
|
|
||||||
uselist=False,
|
|
||||||
single_parent=True,
|
|
||||||
)
|
|
||||||
image = Column(JSONB)
|
image = Column(JSONB)
|
||||||
quantity_min = Column(Integer, nullable=True)
|
quantity_min = Column(Integer, nullable=True)
|
||||||
quantity_min_extra = Column(Integer, nullable=True)
|
quantity_min_extra = Column(Integer, nullable=True)
|
||||||
external_link = Column(String, nullable=True)
|
external_link = Column(String, nullable=True)
|
||||||
hidden = Column(Boolean, default=False)
|
hidden = Column(Boolean, default=False)
|
||||||
|
comment_text = Column(Text, nullable=True)
|
||||||
|
comment_user_id = Column(
|
||||||
|
Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=True
|
||||||
|
)
|
||||||
|
comment_at = Column(DateTime, nullable=True)
|
||||||
created_at = Column(DateTime, default=datetime.now)
|
created_at = Column(DateTime, default=datetime.now)
|
||||||
updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
||||||
refilled_at = Column(DateTime, default=datetime.now)
|
refilled_at = Column(DateTime, default=datetime.now)
|
||||||
|
|||||||
Reference in New Issue
Block a user