комментарии на инструментах
This commit is contained in:
Binary file not shown.
Binary file not shown.
@@ -82,6 +82,24 @@ async def toolkit_request(
|
||||
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="Подготовка заполнения ящика")
|
||||
async def fill_toolbox():
|
||||
logger.info(f"Подготовка заполнения ящика")
|
||||
|
||||
@@ -3823,6 +3823,7 @@ async function getToolkitStocks(toolkitId) {
|
||||
return resp.data;
|
||||
}
|
||||
|
||||
// Функция показа модального окна с деталями инструмента
|
||||
// Функция показа модального окна с деталями инструмента
|
||||
async function showToolkitDetailsModal(toolkitId) {
|
||||
const modalId = 'toolkitDetailsModal';
|
||||
@@ -3920,6 +3921,36 @@ async function showToolkitDetailsModal(toolkitId) {
|
||||
let toolkitStocksData = null;
|
||||
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 = `
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
@@ -3993,6 +4024,27 @@ async function showToolkitDetailsModal(toolkitId) {
|
||||
` : ''}
|
||||
</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 class="modal-footer">
|
||||
<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);
|
||||
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 () => {
|
||||
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 db.handlers.stock import StockHandler
|
||||
from db.handlers.user import UserHandler
|
||||
from utils import logger, saveImage, safeFilename
|
||||
from db import CRUD
|
||||
from db.schemas.toolkit import Toolkit
|
||||
@@ -175,7 +176,12 @@ class ToolkitHandler:
|
||||
if not toolkit:
|
||||
logger.error("Инструмент не найден")
|
||||
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]:
|
||||
query = select(Toolkit).where(Toolkit.id.in_(toolkitIds))
|
||||
@@ -211,6 +217,25 @@ class ToolkitHandler:
|
||||
)
|
||||
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():
|
||||
from .categories import CategoryHandler
|
||||
|
||||
|
||||
+132
-49
@@ -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.user import UserHandler
|
||||
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.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:
|
||||
existing_tables: Optional[list[str]] = None
|
||||
@@ -22,6 +25,10 @@ class DatabaseInitializer:
|
||||
self.engine: Optional[AsyncEngine] = None
|
||||
self.metadata = Base.metadata
|
||||
|
||||
# ==========================================================
|
||||
# PUBLIC
|
||||
# ==========================================================
|
||||
|
||||
async def initialize(self, force: bool = False, reNewDB: bool = False):
|
||||
"""Main database initialization method"""
|
||||
try:
|
||||
@@ -29,32 +36,39 @@ class DatabaseInitializer:
|
||||
|
||||
async with self.engine.begin() as conn:
|
||||
if force:
|
||||
logger.info("Принудительное удаление и создание баз...")
|
||||
logger.warning("Принудительное удаление и создание БД...")
|
||||
await self._drop_all()
|
||||
await self._create_tables_directly()
|
||||
await self._initialize_data(reNewDB)
|
||||
elif not await self._check_tables_exist(conn):
|
||||
logger.info("Не все необходимые таблицы существуют. Создаем...")
|
||||
return
|
||||
|
||||
tables_exist = await self._check_tables_exist(conn)
|
||||
|
||||
if not tables_exist:
|
||||
logger.warning("Не все таблицы существуют. Создаем недостающие...")
|
||||
await self._create_tables_directly()
|
||||
|
||||
# Проверяем после создания
|
||||
async with self.engine.begin() as conn:
|
||||
if not await self._check_tables_exist(conn):
|
||||
raise RuntimeError("Не все необходимые таблицы существуют!")
|
||||
# 🔥 СИНХРОНИЗАЦИЯ СХЕМЫ
|
||||
logger.info("Синхронизация схемы БД...")
|
||||
await self._sync_schema(conn)
|
||||
|
||||
if reNewDB:
|
||||
logger.info("Принудительная загрузка данных...")
|
||||
logger.warning("Принудительная загрузка данных...")
|
||||
await self._initialize_data(reNewDB)
|
||||
else:
|
||||
logger.info("Все необходимые таблицы существуют. Пропускаем...")
|
||||
|
||||
logger.info("Инициализация БД завершена успешно")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Инициализация базы завершилась ошибкой: {str(e)}")
|
||||
logger.exception("Инициализация базы завершилась ошибкой")
|
||||
raise
|
||||
finally:
|
||||
if self.engine:
|
||||
await self.engine.dispose()
|
||||
|
||||
# ==========================================================
|
||||
# CHECK
|
||||
# ==========================================================
|
||||
|
||||
async def _check_tables_exist(self, conn) -> bool:
|
||||
"""Check if all tables from metadata exist"""
|
||||
try:
|
||||
@@ -70,12 +84,8 @@ class DatabaseInitializer:
|
||||
|
||||
missing_tables = required_tables - set(DatabaseInitializer.existing_tables)
|
||||
if missing_tables:
|
||||
logger.warning("Существующие таблицы:")
|
||||
logger.info(DatabaseInitializer.existing_tables)
|
||||
logger.warning("Необходимые таблицы:")
|
||||
logger.info(required_tables)
|
||||
logger.warning("Отсутствующие таблицы:")
|
||||
logger.info(missing_tables)
|
||||
logger.warning(missing_tables)
|
||||
return False
|
||||
|
||||
return True
|
||||
@@ -83,62 +93,135 @@ class DatabaseInitializer:
|
||||
logger.warning(f"Проверка таблиц завершилась ошибкой: {str(e)}")
|
||||
return False
|
||||
|
||||
# ==========================================================
|
||||
# CREATE / DROP
|
||||
# ==========================================================
|
||||
|
||||
async def _create_tables_directly(self):
|
||||
"""Create tables directly using SQLAlchemy (bypass Alembic)"""
|
||||
async with self.engine.begin() as conn:
|
||||
# Создаем все таблицы из метаданных
|
||||
for table in self.metadata.sorted_tables:
|
||||
try:
|
||||
if (
|
||||
DatabaseInitializer.existing_tables
|
||||
and table.name in DatabaseInitializer.existing_tables
|
||||
):
|
||||
logger.debug(
|
||||
f"Таблица {table.name} уже существует. Пропускаем..."
|
||||
)
|
||||
logger.debug(f"Таблица {table.name} уже существует")
|
||||
continue
|
||||
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("Все таблицы успешно созданы")
|
||||
await conn.execute(CreateTable(table))
|
||||
|
||||
async def _drop_all(self):
|
||||
"""Drop all tables"""
|
||||
async with self.engine.begin() as conn:
|
||||
# Получаем список всех таблиц
|
||||
existing_tables = await conn.run_sync(
|
||||
lambda sync_conn: inspect(sync_conn).get_table_names()
|
||||
)
|
||||
|
||||
for table in existing_tables:
|
||||
drop_stmt = text(
|
||||
f'DROP TABLE "{table}" CASCADE'
|
||||
) # Кавычки на случай спец. символов
|
||||
logger.info(f"Удаляем таблицу: {table}")
|
||||
await conn.execute(drop_stmt)
|
||||
logger.warning(f"Удаляем таблицу: {table}")
|
||||
await conn.execute(text(f'DROP TABLE "{table}" CASCADE'))
|
||||
|
||||
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):
|
||||
"""Initialize required data"""
|
||||
|
||||
try:
|
||||
logger.info("Инициализация данных...")
|
||||
logger.warning("Инициализация Прав доступа...")
|
||||
|
||||
await AccessLevelHandler.initialize()
|
||||
logger.warning("Инициализация Пользователей...")
|
||||
await UserHandler.initialize()
|
||||
logger.warning("Инициализация Туллбоксов...")
|
||||
await ToolboxHandler.initialize()
|
||||
logger.warning("Инициализация Категорий...")
|
||||
await CategoryHandler.initialize()
|
||||
logger.warning("Инициализация Инструментов...")
|
||||
await ToolkitHandler.initialize()
|
||||
logger.warning("Инициализация Складов...")
|
||||
await StocksActions.initialize()
|
||||
|
||||
logger.info("Данные успешно инициализированы")
|
||||
except Exception as e:
|
||||
logger.error(f"Инициализация данных завершилась ошибкой: {str(e)}")
|
||||
except Exception:
|
||||
logger.exception("Ошибка инициализации данных")
|
||||
raise
|
||||
|
||||
Binary file not shown.
@@ -14,18 +14,16 @@ class Toolkit(Base):
|
||||
description = Column(Text, nullable=True)
|
||||
specifications = Column(JSONB, default={})
|
||||
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)
|
||||
quantity_min = Column(Integer, nullable=True)
|
||||
quantity_min_extra = Column(Integer, nullable=True)
|
||||
external_link = Column(String, nullable=True)
|
||||
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)
|
||||
updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
|
||||
refilled_at = Column(DateTime, default=datetime.now)
|
||||
|
||||
Reference in New Issue
Block a user