создание, правка, удаление, скрытие инструмента

This commit is contained in:
2025-12-12 23:50:38 +03:00
parent 8b38d69980
commit f85ca7d002
19 changed files with 1607 additions and 162 deletions
+19 -1
View File
@@ -1,4 +1,5 @@
from fastapi import APIRouter, Depends, Request from fastapi import APIRouter, Depends, Request
from fastapi.responses import RedirectResponse
from db.handlers.categories import CategoryHandler from db.handlers.categories import CategoryHandler
from utils import render, requestDict, logger from utils import render, requestDict, logger
@@ -18,7 +19,24 @@ router.include_router(toolkit, prefix="/toolkit", tags=["toolkit"])
@router.get("/") @router.get("/")
async def main_page(request: Request): async def main_page(request: Request):
return await render(request) cookies = request.cookies
checkList = ["toolbox_user", "toolbox_access"]
if all(key in cookies for key in checkList):
return await render(request)
else:
for key in checkList:
if key in cookies:
deleteCookie = key
break
else:
deleteCookie = None
if deleteCookie:
response = RedirectResponse(url="/user/login", status_code=302)
response.set_cookie(deleteCookie, "", expires=0)
return response
else:
return RedirectResponse(url="/user/login", status_code=302)
@router.post("/") @router.post("/")
Binary file not shown.
Binary file not shown.
+93 -1
View File
@@ -9,6 +9,28 @@ from utils import requestDict, logger
router = APIRouter() router = APIRouter()
def handleResult(result: dict, response: dict) -> dict:
if "errorMessage" in result.keys():
response["message"] = result["errorMessage"]
else:
response["status"] = "ok"
return response
@router.get("/", summary="Получение инструмента")
async def get_toolkit(reqData: dict = Depends(requestDict)):
logger.info(f"Получение инструмента")
response = {"status": "error"}
toolkitId = reqData.get("query").get("toolkitId")
if toolkitId:
toolkit = await ToolkitHandler.get(int(toolkitId))
if toolkit:
# logger.info(toolkit)
response["status"] = "ok"
response["data"] = toolkit
return response
@router.post("/", summary="Запрос остатка инструмента") @router.post("/", summary="Запрос остатка инструмента")
async def toolkit_request( async def toolkit_request(
reqData: dict = Depends(requestDict), reqData: dict = Depends(requestDict),
@@ -16,7 +38,6 @@ async def toolkit_request(
response = {"status": "error", "data": {}} response = {"status": "error", "data": {}}
toolkitId = reqData.get("body").get("toolkitId") toolkitId = reqData.get("body").get("toolkitId")
logger.info(f"Получение запроса остатка инструмента #{toolkitId}") logger.info(f"Получение запроса остатка инструмента #{toolkitId}")
# logger.info(request_data)
stocks = await StockHandler.getByToolkitId(toolkitId) stocks = await StockHandler.getByToolkitId(toolkitId)
if not stocks: if not stocks:
return response return response
@@ -100,3 +121,74 @@ async def categories_batch(reqData: dict = Depends(requestDict)):
if success: if success:
response["status"] = "ok" response["status"] = "ok"
return response return response
@router.get("/categories", summary="Получение категорий")
async def get_categories():
logger.info(f"Получение категорий")
response = {"status": "error"}
categories = await CategoryHandler.getAll()
if categories:
categoriesDict = {
category["id"]: {
"id": category["id"],
"title": category["title"],
"description": category["description"],
}
for category in categories
}
response["status"] = "ok"
response["data"] = categoriesDict
return response
@router.post("/hide", summary="Скрытие инструмента")
async def hide_toolkit(reqData: dict = Depends(requestDict)):
logger.info(f"Скрытие/отображение инструмента")
response = {"status": "error"}
toolkitId = int(reqData.get("body").get("toolkitId"))
userId = reqData.get("body").get("userId")
hidden = reqData.get("body").get("hidden")
result = await ToolkitHandler.hideToolkit(userId, toolkitId, hidden)
response = handleResult(result, response)
return response
@router.post("/manage", summary="Управление инструментами")
async def manage_toolkit(reqData: dict = Depends(requestDict)):
logger.info(f"Управление инструментами")
response = {"status": "error"}
action = reqData.get("body").get("action")
userId = reqData.get("body").get("UserId")
toolkitData = reqData.get("body").get("formData")
if "category_id" in toolkitData:
toolkitData["category_id"] = int(toolkitData.get("category_id"))
if "image" in toolkitData:
if (
not toolkitData.get("image").get("main")
or toolkitData.get("image").get("main") == ""
):
if len(toolkitData.get("image").get("additional")) == 0:
toolkitData.pop("image")
match action:
case "create":
toolkit = await ToolkitHandler.add(toolkitData, userId)
response = handleResult(toolkit, response)
case "copy":
toolkitData.pop("id")
toolkit = await ToolkitHandler.add(toolkitData, userId)
response = handleResult(toolkit, response)
case "update":
toolkit = await ToolkitHandler.edit(userId, **toolkitData)
response = handleResult(toolkit, response)
case "delete":
toolkit = await ToolkitHandler.delete(toolkitData.get("id"), userId)
response = handleResult(toolkit, response)
case _:
pass
logger.info(
f"Управление инструментами ({action}) прошло {'успешно' if response.get('status') == 'ok' else 'неуспешно'}"
)
return response
Binary file not shown.

After

Width:  |  Height:  |  Size: 342 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

+16 -3
View File
@@ -1,14 +1,27 @@
// api.js // api.js
export async function apiRequest(url, payload = {}, method = 'POST') { export async function apiRequest(url, payload = {}, method = 'POST') {
const res = await fetch(url, { method = method.toUpperCase();
let finalUrl = url;
let options = {
method, method,
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Accept': 'application/json' 'Accept': 'application/json'
}, },
body: JSON.stringify(payload),
credentials: 'same-origin' credentials: 'same-origin'
}); };
// --- Если GET → добавляем payload в URL ---
if (method === 'GET') {
const params = new URLSearchParams(payload);
finalUrl = `${url}?${params.toString()}`;
} else {
// --- Для остальных методов → отправляем body ---
options.body = JSON.stringify(payload);
}
const res = await fetch(finalUrl, options);
if (!res.ok) { if (!res.ok) {
const text = await res.text(); const text = await res.text();
+17 -7
View File
@@ -23,16 +23,26 @@ export async function setCookie(name, value, days = 180) {
} }
} }
const secure = true; // TODO включить после тестов
const sameSite = 'Lax';
const expires = new Date(Date.now() + days * 864e5).toUTCString(); const expires = new Date(Date.now() + days * 864e5).toUTCString();
const encodedName = encodeURIComponent(name);
let cookie = `${encodeURIComponent(name)}=${cookieValue}; expires=${expires}; path=/`; // ---------- 1. Пытаемся установить безопасную куку ----------
if (secure) cookie += '; Secure'; let secureCookie = `${encodedName}=${cookieValue}; expires=${expires}; path=/; Secure; SameSite=Lax`;
if (sameSite) cookie += `; SameSite=${sameSite}`; document.cookie = secureCookie;
document.cookie = cookie; // ---------- 2. Проверяем, записалась ли она ----------
const isSet = document.cookie.split('; ')
.some(c => c.startsWith(`${encodedName}=`));
if (isSet) {
return true; // безопасная кука успешно установлена
}
// ---------- 3. Фолбэк: ставим обычную (без Secure) ----------
let normalCookie = `${encodedName}=${cookieValue}; expires=${expires}; path=/; SameSite=Lax`;
document.cookie = normalCookie;
return false; // безопасную куку установить не удалось
} }
export async function getCookie(name) { export async function getCookie(name) {
+1373 -95
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.
Binary file not shown.
+7
View File
@@ -138,6 +138,13 @@ class StockHandler:
stocks = await CRUD.read(query, True) stocks = await CRUD.read(query, True)
return await filterQuantity(stocks, filtered) return await filterQuantity(stocks, filtered)
async def checkToolkitExists(toolkitId: int) -> bool:
from db import CRUD
query = select(Stock).where(Stock.toolkit_id == toolkitId)
stocks = await CRUD.read(query)
return True if stocks else False
async def getByToolboxIdAndToolkitId( async def getByToolboxIdAndToolkitId(
toolboxId: int, toolkitId: int, filtered: bool = True toolboxId: int, toolkitId: int, filtered: bool = True
) -> list[dict]: ) -> list[dict]:
+73 -47
View File
@@ -1,15 +1,24 @@
from datetime import datetime from datetime import datetime
from utils import logger, saveImage, safeFilename, deleteImage from db.handlers.stock import StockHandler
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
from sqlalchemy import select from sqlalchemy import select
from db.handlers.records import ServiceRecordsHandler from db.handlers.records import ServiceRecordsHandler
from utils.image import deleteImage
def handleToolkitImage(imageData, title: str): def handleToolkitImage(imageData, title: str):
import base64
title = safeFilename(title) title = safeFilename(title)
fileName = f"tools/{title}.png" fileName = f"static/images/tools/{title}.png"
if not saveImage(imageData, fileName): if imageData.startswith("data:image"):
header, encoded = imageData.split(",", 1)
else:
encoded = imageData
file_bytes = base64.b64decode(encoded)
if not saveImage(file_bytes, fileName):
return None return None
return fileName return fileName
@@ -19,22 +28,20 @@ class ToolkitHandler:
title = toolkitData.get("title", None) title = toolkitData.get("title", None)
if not title: if not title:
logger.error("Не указано название инструмента") logger.error("Не указано название инструмента")
return {} return {"errorMessage": "Не указано название инструмента"}
query = select(Toolkit).where(Toolkit.title == title) query = select(Toolkit).where(Toolkit.title == title)
toolkit = await CRUD.read(query) toolkit = await CRUD.read(query)
if toolkit: if toolkit:
logger.error("Инструмент с таким названием уже существует") logger.error("Инструмент с таким названием уже существует")
return {} return {"errorMessage": "Инструмент с таким названием уже существует"}
try: try:
imageDict = {"main": "static/images/tools/default.png", "additional": []} imageDict = {"main": "static/images/tools/default.png", "additional": []}
if "image" in toolkitData: if "image" in toolkitData:
imageData = toolkitData.pop("image") imageData = toolkitData.pop("image")
mainImage = imageData.get("main") mainImage = imageData.get("main")
if isinstance(mainImage, str) and mainImage.startswith( if mainImage.startswith("static/images/"):
"static/images/"
):
imageDict["main"] = mainImage imageDict["main"] = mainImage
else: else:
imageFileName = handleToolkitImage(mainImage, title) imageFileName = handleToolkitImage(mainImage, title)
@@ -43,9 +50,7 @@ class ToolkitHandler:
additionalImages = imageData.get("additional", []) additionalImages = imageData.get("additional", [])
if len(additionalImages) > 0: if len(additionalImages) > 0:
for image in additionalImages: for image in additionalImages:
if isinstance(image, str) and image.startswith( if image.startswith("static/images/"):
"static/images/"
):
imageDict["additional"].append(image) imageDict["additional"].append(image)
else: else:
imageFileName = handleToolkitImage(image, title) imageFileName = handleToolkitImage(image, title)
@@ -55,18 +60,22 @@ class ToolkitHandler:
newToolkit = await Toolkit(**toolkitData).save() newToolkit = await Toolkit(**toolkitData).save()
except Exception as e: except Exception as e:
logger.error(f"Ошибка сохранения инструмента: {str(e)}") logger.error(f"Ошибка сохранения инструмента: {str(e)}")
return {} return {"errorMessage": f"Ошибка сохранения инструмента: {str(e)}"}
if not newToolkit: if not newToolkit:
logger.error("Инструмент не сохранен") logger.error("Инструмент не сохранен")
return {} return {"errorMessage": "Инструмент не сохранен"}
logger.info(f"Инструмент {newToolkit.title} успешно создан") logger.info(f"Инструмент {newToolkit.title} успешно создан")
await ServiceRecordsHandler.add(user_id, {"Добавлен инструмент": toolkitData}) await ServiceRecordsHandler.add(user_id, {"Добавлен инструмент": toolkitData})
return newToolkit return newToolkit.toDict()
async def updateMovindDate(toolkitId: int): async def updateMovindDate(toolkitId: int):
editedToolkit = await ToolkitHandler.edit(toolkitId, moved_at=datetime.now()) toolkit = await CRUD.read(select(Toolkit).where(Toolkit.id == toolkitId))
if not toolkit:
logger.error("Инструмент не найден")
return False
editedToolkit = await toolkit.edit(moved_at=datetime.now())
if not editedToolkit: if not editedToolkit:
logger.error("Инструмент не обновлен") logger.error("Инструмент не обновлен")
return False return False
@@ -74,65 +83,78 @@ class ToolkitHandler:
async def updateRefillDate(toolkitId: int): async def updateRefillDate(toolkitId: int):
logger.info(f"Обновление даты пополнения инструмента {toolkitId}...") logger.info(f"Обновление даты пополнения инструмента {toolkitId}...")
editedToolkit = await ToolkitHandler.edit(toolkitId, refilled_at=datetime.now()) toolkit = await CRUD.read(select(Toolkit).where(Toolkit.id == toolkitId))
if not toolkit:
logger.error("Инструмент не найден")
return False
editedToolkit = await toolkit.edit(refilled_at=datetime.now())
if not editedToolkit: if not editedToolkit:
logger.error("Инструмент не обновлен") logger.error("Инструмент не обновлен")
return False return False
return True return True
async def edit(toolkitId: int, **kwargs): async def hideToolkit(userId: int, toolkitId: int, hidden: bool = True):
logger.info(
f"{'Скрытие' if hidden else 'Отображение'} инструмента {toolkitId}..."
)
return await ToolkitHandler.edit(userId, id=toolkitId, hidden=hidden)
async def edit(user_id: int, **kwargs):
title = kwargs.get("title", None)
toolkitId = kwargs.pop("id")
if title:
query = select(Toolkit).where(Toolkit.title == title)
toolkit = await CRUD.read(query)
if toolkit:
if toolkit.id != toolkitId:
logger.error("Инструмент с таким названием уже существует")
return {
"errorMessage": "Инструмент с таким названием уже существует"
}
query = select(Toolkit).where(Toolkit.id == toolkitId) query = select(Toolkit).where(Toolkit.id == toolkitId)
toolkit = await CRUD.read(query) toolkit = await CRUD.read(query)
if not toolkit: if not toolkit:
logger.error("Инструмент не найден") logger.error("Инструмент не найден")
return {} return {"errorMessage": "Инструмент не найден"}
try: try:
if "image" in kwargs: if "image" in kwargs:
title = kwargs.get("title", toolkit.title) title = kwargs.get("title", toolkit.title)
imageData = kwargs.pop("image") imageData = kwargs.pop("image")
imageDict = {"main": "", "additional": []} imageDict = {"main": "", "additional": []}
existImagesList = [toolkit.image.get("main")] if imageData.get("main").startswith("static/images/"):
existImagesList.extend(toolkit.image.get("additional")) imageDict["main"] = imageData.get("main")
else:
imageFileName = handleToolkitImage(imageData.get("main"), title)
if imageFileName:
imageDict["main"] = imageFileName
deleteImage(toolkit.image.get("main"))
newImagesList = [imageData.get("main")] for image in imageData.get("additional"):
newImagesList.extend(imageData.get("additional")) if image.startswith("static/images/"):
imageDict["additional"].append(image)
for existImage in existImagesList:
if existImage not in newImagesList:
deleteImage(existImage)
if toolkit.image.get("main") != imageData.get("main"):
if imageData.get("main") in existImagesList:
imageDict["main"] = imageData.get("main")
else: else:
imageFileName = handleToolkitImage(imageData.get("main"), title)
if imageFileName:
imageDict["main"] = imageFileName
else:
imageDict["main"] = "images/tools/default.png"
imageDict["additional"].extend(imageData.get("additional"))
uploadList = imageData.get("upload", [])
if len(uploadList) > 0:
for image in uploadList:
imageFileName = handleToolkitImage(image, title) imageFileName = handleToolkitImage(image, title)
if imageFileName: if imageFileName:
imageDict["additional"].append(imageFileName) imageDict["additional"].append(imageFileName)
for existImage in toolkit.image.get("additional"):
if existImage not in imageDict.get("additional"):
deleteImage(existImage)
kwargs["image"] = imageDict kwargs["image"] = imageDict
user_id = kwargs.pop("user_id", None)
logger.debug(f"Обновление инструмента {toolkit.title}...") logger.debug(f"Обновление инструмента {toolkit.title}...")
editedToolkit = await toolkit.edit(**kwargs) editedToolkit = await toolkit.edit(**kwargs)
except Exception as e: except Exception as e:
logger.error(f"Ошибка обновления инструмента: {str(e)}") logger.error(f"Ошибка обновления инструмента: {str(e)}")
return {} return {"errorMessage": f"Ошибка обновления инструмента: {str(e)}"}
if not editedToolkit: if not editedToolkit:
logger.error("Инструмент не обновлен") logger.error("Инструмент не обновлен")
return {} return {"errorMessage": "Инструмент не обновлен"}
logger.info( logger.info(
f"Инструмент {editedToolkit.title} успешно обновлен, изменены данные: {kwargs.keys()}" f"Инструмент {editedToolkit.title} успешно обновлен, изменены данные: {kwargs.keys()}"
@@ -166,24 +188,28 @@ class ToolkitHandler:
return True if toolkit else False return True if toolkit else False
async def delete(toolkitId: int, user_id: int = None): async def delete(toolkitId: int, user_id: int = None):
movements = await StockHandler.checkToolkitExists(toolkitId)
if movements:
logger.error("По инструменту было движение")
return {"errorMessage": "По инструменту было движение"}
query = select(Toolkit).where(Toolkit.id == toolkitId) query = select(Toolkit).where(Toolkit.id == toolkitId)
toolkit = await CRUD.read(query) toolkit = await CRUD.read(query)
if not toolkit: if not toolkit:
logger.error("Инструмент не найден") logger.error("Инструмент не найден")
return False return {"errorMessage": "Инструмент не найден"}
try: try:
toolkitTitle = toolkit.title toolkitTitle = toolkit.title
result = await CRUD.delete(toolkit) result = await CRUD.delete(toolkit)
except Exception as e: except Exception as e:
logger.error(f"Ошибка удаления инструмента: {str(e)}") logger.error(f"Ошибка удаления инструмента: {str(e)}")
return False return {"errorMessage": f"Ошибка удаления инструмента: {str(e)}"}
logger.info( logger.info(
f"Инструмент {toolkitTitle} {'успешно удален' if result else 'не удален'}" f"Инструмент {toolkitTitle} {'успешно удален' if result else 'не удален'}"
) )
await ServiceRecordsHandler.add( await ServiceRecordsHandler.add(
user_id, {"Удален инструмент": f"Название: {toolkitTitle}"} user_id, {"Удален инструмент": f"Название: {toolkitTitle}"}
) )
return result return {"status": "ok"} if result else {"errorMessage": "Инструмент не удален"}
async def initialize(): async def initialize():
from .categories import CategoryHandler from .categories import CategoryHandler
Binary file not shown.
+2 -1
View File
@@ -1,5 +1,5 @@
from datetime import datetime from datetime import datetime
from sqlalchemy import Column, DateTime, ForeignKey, Integer, String, Text from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, String, Text
from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from db import Base, CRUD from db import Base, CRUD
@@ -25,6 +25,7 @@ class Toolkit(Base):
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)
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)
+2 -2
View File
@@ -23,8 +23,8 @@ async def main():
from db.initialize import DatabaseInitializer from db.initialize import DatabaseInitializer
try: try:
force = False force = True
reNewDB = False reNewDB = True
await DatabaseInitializer(DATABASE_URL).initialize(force, reNewDB) await DatabaseInitializer(DATABASE_URL).initialize(force, reNewDB)
except Exception as e: except Exception as e:
logger.error(f"Инициализация базы завершилась ошибкой: {str(e)}", exc_info=True) logger.error(f"Инициализация базы завершилась ошибкой: {str(e)}", exc_info=True)
Binary file not shown.
+5 -5
View File
@@ -32,7 +32,7 @@ def saveImage(file_bytes: bytes, file_name: str) -> bool:
if not target_path.lower().endswith(".png"): if not target_path.lower().endswith(".png"):
target_path += ".png" target_path += ".png"
logger.info(f"[ImageSave] Saving image to {target_path}") logger.debug(f"[ImageSave] Saving image to {target_path}")
img.save(target_path, "PNG") img.save(target_path, "PNG")
return True return True
@@ -47,10 +47,10 @@ def deleteImage(fileName: str):
try: try:
import os import os
file_name = f"api/{file_name}" fileName = f"api/{fileName}"
logger.info(f"Удаляем изображение {fileName}") logger.debug(f"Удаляем изображение {fileName}")
os.remove(f"static/images/{fileName}") os.remove(fileName)
logger.info(f"Изображение {fileName} успешно удалено") logger.debug(f"Изображение {fileName} успешно удалено")
return True return True
except Exception as e: except Exception as e:
logger.error(f"Ошибка удаления изображения: {str(e)}") logger.error(f"Ошибка удаления изображения: {str(e)}")