комментарии на инструментах

This commit is contained in:
2025-12-20 23:22:51 +03:00
parent d7bfdb69a6
commit 83ecf65abf
11 changed files with 336 additions and 65 deletions
Binary file not shown.
Binary file not shown.
+18
View File
@@ -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"Подготовка заполнения ящика")
+147
View File
@@ -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
View File
@@ -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
+140 -57
View File
@@ -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("Принудительная загрузка данных...")
await self._initialize_data(reNewDB)
else:
logger.info("Все необходимые таблицы существуют. Пропускаем...")
if reNewDB:
logger.warning("Принудительная загрузка данных...")
await self._initialize_data(reNewDB)
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} уже существует. Пропускаем..."
)
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("Все таблицы успешно созданы")
if (
DatabaseInitializer.existing_tables
and table.name in DatabaseInitializer.existing_tables
):
logger.debug(f"Таблица {table.name} уже существует")
continue
logger.info(f"Создаем таблицу: {table.name}")
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.
+5 -7
View File
@@ -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)