diff --git a/api/routers/__init__.py b/api/routers/__init__.py index 141bcbf..deaeac6 100644 --- a/api/routers/__init__.py +++ b/api/routers/__init__.py @@ -2,6 +2,7 @@ from datetime import date, datetime, timedelta from fastapi import APIRouter, Depends, Request from fastapi.responses import RedirectResponse +from db.handlers.access import AccessLevelHandler from db.handlers.categories import CategoryHandler from utils import render, requestDict, logger from .user import router as user @@ -181,14 +182,24 @@ async def post_requests( "startDate": startDate.strftime("%Y-%m-%d"), "endDate": endDate.strftime("%Y-%m-%d"), } - # logger.info(resultData.get("data")) case "users": users = await UserHandler.getAll() - if users: + if isinstance(users, list): + if len(users) == 0: + resultData["status"] = "ok" + resultData["data"] = { + "users": [], + "accessLevels": [], + } for user in users: user.pop("hashed_password") + accessLevels = await AccessLevelHandler.getAll() resultData["status"] = "ok" - resultData["data"] = users + resultData["data"] = { + "users": users, + "accessLevels": accessLevels, + } + # logger.info(resultData) case _: pass return resultData diff --git a/api/routers/__pycache__/__init__.cpython-313.pyc b/api/routers/__pycache__/__init__.cpython-313.pyc index 8f8d2ea..3eb2c86 100644 Binary files a/api/routers/__pycache__/__init__.cpython-313.pyc and b/api/routers/__pycache__/__init__.cpython-313.pyc differ diff --git a/api/routers/__pycache__/user.cpython-313.pyc b/api/routers/__pycache__/user.cpython-313.pyc index ef27032..51ca486 100644 Binary files a/api/routers/__pycache__/user.cpython-313.pyc and b/api/routers/__pycache__/user.cpython-313.pyc differ diff --git a/api/routers/user.py b/api/routers/user.py index 3c60f5a..36025bc 100644 --- a/api/routers/user.py +++ b/api/routers/user.py @@ -12,9 +12,62 @@ async def get_user(): return -@router.post("/") -async def create_user(): - return +@router.post("/", summary="Правка данных пользователя") +async def manage_user(request_data: dict = Depends(requestDict)): + response = {"status": "error"} + userData = request_data.get("body").get("userData", {}) + action = request_data.get("body").get("action") + userId = request_data.get("body").get("userId") + match action: + case "create": + result = await UserHandler.add(userData, userId) + if result: + response["status"] = "ok" + case "update": + result = await UserHandler.edit(userData, user_id=userId) + if "error" not in result: + response["status"] = "ok" + else: + response["message"] = result["error"] + case "delete": + result = await UserHandler.delete(userData["id"], userId) + if "error" not in result: + response["status"] = "ok" + else: + response["message"] = result["error"] + case _: + logger.error(f"Неверное действие: {action}") + return response + + +@router.post("/level", summary="Правка уровня доступа") +async def manage_access_level(request_data: dict = Depends(requestDict)): + logger.info(request_data.get("body")) + action = request_data.get("body").get("action") + userId = request_data.get("body").get("userId") + levelData = request_data.get("body").get("changedLevelData") + match action: + case "create": + result = await AccessLevelHandler.add(levelData, userId) + if "error" not in result: + return {"status": "ok"} + else: + return {"status": "error", "message": result["error"]} + case "update": + result = await AccessLevelHandler.edit(levelData, userId) + if "error" not in result: + return {"status": "ok"} + else: + return {"status": "error", "message": result["error"]} + case "delete": + result = await AccessLevelHandler.delete(levelData["id"], userId) + if "error" not in result: + return {"status": "ok"} + else: + return {"status": "error", "message": result["error"]} + case _: + logger.error(f"Неверное действие: {action}") + return {"status": "ok"} @router.get("/login", name="Authentication", summary="Авторизация пользователя") diff --git a/api/static/images/tools/plastina_cnmg_120408_1765724430069.png b/api/static/images/tools/plastina_cnmg_120408_1765724430069.png new file mode 100644 index 0000000..a47eb75 Binary files /dev/null and b/api/static/images/tools/plastina_cnmg_120408_1765724430069.png differ diff --git a/api/static/js/index.js b/api/static/js/index.js index ce3770b..cc2da21 100644 --- a/api/static/js/index.js +++ b/api/static/js/index.js @@ -181,7 +181,7 @@ function prepareTabs() {
-
+
@@ -267,7 +267,7 @@ function fillTab(tabId, tabData) { renderJurnalServicesTab(tabId, tabData); break; case 'users': - renderSimpleTab(tabId, tabData, 'Пользователи системы'); + renderUsersTab(tabId, tabData); break; } } catch (error) { @@ -282,35 +282,6 @@ function fillTab(tabId, tabData) { } } -function renderSimpleTab(tabId, tabData, title) { - const tabContent = document.getElementById(`${tabId}-tab-content`); - tabContent.innerHTML = ` -
-
-
-
-
${title}
-
- ${Object.entries(tabData).map(([key, value]) => ` -
-
-
-
${key}
-

- ${typeof value === 'object' ? JSON.stringify(value, null, 4) : value} -

-
-
-
- `).join('')} -
-
-
-
-
- `; -} - async function manageCategory(categoriesList) { // Удаляем старое модальное окно, если оно существует @@ -1421,7 +1392,7 @@ function renderToolkitCards(tabId, tools, categoriesMap, filterText = '', catego data-toolid="${tool.id}">
${tool.title || 'Инструмент'} @@ -3714,7 +3685,7 @@ async function showToolkitDetailsModal(toolkitId) { ${toolkitData.title} + style="max-height: 300px; object-fit: cover; cursor: zoom-in;">
`).join('')} @@ -3747,7 +3718,7 @@ async function showToolkitDetailsModal(toolkitId) { ${toolkitData.title} + style="max-height: 300px; object-fit: cover; cursor: zoom-in;">
` : '
'; @@ -4272,7 +4243,7 @@ async function manageToolkit(toolkitData = null, categories = null, action = 'cr
+ style="max-height: 150px; object-fit: cover;"> + ` : ''} + ${accessData.access_level_view ? ` + + ` : ''} +
+
+
+ `; + + // Функция для показа модального окна с подтверждением + function showConfirmationModal(title, message, confirmCallback) { + const modalId = `${tabId}-confirmation-modal`; + + // Удаляем старую модалку, если есть + const existingModal = document.getElementById(modalId); + if (existingModal) { + existingModal.remove(); + } + + const modalHTML = ` + + `; + + document.body.insertAdjacentHTML('beforeend', modalHTML); + const modal = new bootstrap.Modal(document.getElementById(modalId)); + + document.getElementById(`${modalId}-confirm-btn`).addEventListener('click', () => { + confirmCallback(); + modal.hide(); + }); + + modal.show(); + + // Удаляем модалку после скрытия + modal._element.addEventListener('hidden.bs.modal', () => { + modal._element.remove(); + }); + } + + // Функция для рендеринга карточек пользователей + function renderUsersCards() { + if (!users || users.length === 0) { + tabContent.innerHTML = ` + + `; + return; + } + + tabContent.innerHTML = ` +
+ ${users.map(user => { + const isActive = user.is_active; + const levelTitle = accessLevelMap[user.access_level_id] || `Уровень ${user.access_level_id}`; + const createdAt = new Date(user.created_at).toLocaleDateString('ru-RU'); + + return ` +
+
+
+
+ ${user.username} +
+
+
+
${user.username || 'Без имени'}
+
${user.login || 'Без логина'}
+
+
+
+
+ +
+
+ + ${levelTitle} +
+
+ + Создан: ${createdAt} +
+
+ + Обновлен: ${user.updated_at ? new Date(user.updated_at).toLocaleDateString('ru-RU') : 'Нет данных'} +
+
+ +
+ ${accessData.users_edit ? ` + + ` : ''} + + ${accessData.users_disabling ? ` + + ` : ''} +
+
+
+
+ `; + }).join('')} +
+ `; + + // Назначаем обработчики для кнопок в карточках + document.querySelectorAll(`.edit-user-btn`).forEach(btn => { + btn.addEventListener('click', async () => { + const userId = btn.dataset.userId; + const user = users.find(u => u.id == userId); + if (user) { + await openEditUserModal(user); + } + }); + }); + + document.querySelectorAll(`.toggle-user-btn`).forEach(btn => { + btn.addEventListener('click', async () => { + const userId = btn.dataset.userId; + const isActive = btn.dataset.isActive === 'true'; + const user = users.find(u => u.id == userId); + if (user) { + const action = isActive ? 'заблокировать' : 'разблокировать'; + showConfirmationModal( + `${isActive ? 'Блокировка' : 'Разблокировка'} пользователя`, + `Вы уверены, что хотите ${action} пользователя ${user.username}?`, + () => toggleUserStatus(userId, !isActive) + ); + } + }); + }); + } + + // Функция для открытия модального окна редактирования пользователя + async function openEditUserModal(user = null) { + const isNew = !user; + const modalId = `${tabId}-edit-user-modal`; + + // Удаляем старую модалку, если есть + const existingModal = document.getElementById(modalId); + if (existingModal) { + existingModal.remove(); + } + + const modalHTML = ` + + `; + + document.body.insertAdjacentHTML('beforeend', modalHTML); + const modal = new bootstrap.Modal(document.getElementById(modalId)); + let photoFile = null; + let removePhoto = false; + + // Обработчики для фото + if (accessData.users_edit) { + const changePhotoBtn = document.getElementById(`${modalId}-change-photo-btn`); + const photoInput = document.getElementById(`${modalId}-photo-input`); + const photoPreview = document.getElementById(`${modalId}-photo-preview`); + + changePhotoBtn.addEventListener('click', () => { + photoInput.click(); + }); + + photoInput.addEventListener('change', (e) => { + const file = e.target.files[0]; + if (file) { + if (file.type.startsWith('image/')) { + photoFile = file; + const reader = new FileReader(); + reader.onload = (e) => { + photoPreview.src = e.target.result; + }; + reader.readAsDataURL(file); + removePhoto = false; + } else { + showConfirmationModal('Ошибка', 'Пожалуйста, выберите файл изображения', () => { }); + photoInput.value = ''; + } + } + }); + + if (document.getElementById(`${modalId}-remove-photo-btn`)) { + document.getElementById(`${modalId}-remove-photo-btn`).addEventListener('click', () => { + photoPreview.src = 'static/images/users/default.png'; + photoFile = null; + removePhoto = true; + }); + } + } + + // Обработчик удаления пользователя + if (!isNew && accessData.users_disabling) { + document.getElementById(`${modalId}-delete-btn`).addEventListener('click', () => { + modal.hide(); + showConfirmationModal( + 'Удаление пользователя', + `Вы уверены, что хотите удалить пользователя ${user.username}? Это действие нельзя отменить.`, + () => deleteUser(user.id) + ); + }); + } + + // Обработчик сохранения + document.getElementById(`${modalId}-save-btn`).addEventListener('click', async () => { + const form = document.getElementById(`${modalId}-form`); + if (!form.checkValidity()) { + form.reportValidity(); + return; + } + + const formUserData = { + login: document.getElementById(`${modalId}-login`).value.trim(), + username: document.getElementById(`${modalId}-username`).value.trim(), + access_level_id: parseInt(document.getElementById(`${modalId}-access-level`).value), + }; + + if (isNew || ((user && formUserData.login != user.login))) { + if (userLogins.includes(formUserData.login)) { + showInfo('Пользователь с таким логином уже существует', 'warning'); + return; + } + } + if (isNew || ((user && formUserData.username != user.username))) { + if (userUsernames.includes(formUserData.username)) { + showInfo('Пользователь с таким именем уже существует', 'warning'); + return; + } + } + + // Добавляем пароль, если он указан + const password = document.getElementById(`${modalId}-password`).value; + if (password || isNew) { + formUserData.password = password || ''; + } + + // Определяем действие + let action = isNew ? 'create' : 'update'; + + // Обработка фото + if (photoFile) { + if (photoFile.size > 5 * 1024 * 1024) { + showInfo('Фото больше 5 МБ, оно не подходит', 'warning'); + return; + } + formUserData.photo = await new Promise((resolve, reject) => { + const reader = new FileReader(); + + reader.onload = () => resolve(reader.result); + reader.onerror = () => reject(new Error('Ошибка чтения файла')); + + reader.readAsDataURL(photoFile); + }); + + } else if (removePhoto) { + formUserData.photo = ''; + } + + let changedUserData = {}; + if (isNew) { + changedUserData = formUserData; + } else { + changedUserData = Object.keys(formUserData).reduce((acc, key) => { + if (formUserData[key] !== user[key]) { + acc[key] = formUserData[key]; + } + return acc; + }, {}); + } + + if (Object.keys(changedUserData).length === 0) { + showInfo('Нет изменений', 'info'); + return; + } + + // Обновляем данные + if (!isNew) { + changedUserData.id = user.id; + } + + await sendUserRequest(changedUserData, modal, action); + }); + + modal.show(); + + // Удаляем модалку после скрытия + modal._element.addEventListener('hidden.bs.modal', () => { + modal._element.remove(); + }); + } + + // Функция для обновления данных + async function refreshThisTab() { + // Обновляем данные + const newData = await apiRequest('/', { tabId: 'users' }); + if (newData && newData.status === 'ok') { + renderUsersTab(tabId, newData.data); + } else { + showInfo('Не удалось загрузить обновленные данные', 'error'); + } + } + + // Функция для отправки запроса на изменение пользователя + const currentUserId = userData.id; + + async function sendUserRequest(userData, modal, action) { + const result = await apiRequest('/user/', { action, userData, userId: currentUserId }); + + const actionTextMap = { + create: 'создан', + update: 'обновлен', + remove_photo: 'обновлен', + delete: 'удален' + } + if (result && result.status === 'ok') { + if (modal) { modal.hide(); } + showInfo('Пользователь успешно ' + actionTextMap[action], 'success'); + await refreshThisTab(); + } else { + const errorMsg = result?.message || 'Произошла ошибка при ' + actionTextMap[action] + 'ии'; + showInfo(errorMsg, 'error'); + } + } + + // Функция для удаления пользователя + async function deleteUser(userId) { + await sendUserRequest({ id: userId }, null, 'delete'); + } + + // Функция для блокировки/разблокировки пользователя + async function toggleUserStatus(userId, newStatus) { + await sendUserRequest({ id: userId, is_active: newStatus }, null, 'update'); + } + + // Функция для открытия модального окна управления уровнями доступа + async function openAccessLevelsModal() { + const modalId = `${tabId}-access-levels-modal`; + + // Удаляем старую модалку, если есть + const existingModal = document.getElementById(modalId); + if (existingModal) { + existingModal.remove(); + } + + // Создаем копию уровней доступа для редактирования + const editableLevels = JSON.parse(JSON.stringify(accessLevels)); + let newLevelMode = false; + let currentLevelId = editableLevels[0]?.id; + + const modalHTML = ` + + `; + + document.body.insertAdjacentHTML('beforeend', modalHTML); + const modal = new bootstrap.Modal(document.getElementById(modalId)); + + // Функция для загрузки содержимого уровня + function loadLevelContent(levelId, isNew = false) { + let level; + if (isNew) { + level = { + id: 'new', + title: '', + description: '', + tools_creation: false, + tools_registration: false, + tools_edit: false, + tools_delete: false, + users_creation: false, + users_edit: false, + users_disabling: false, + users_view: false, + available_own_toolbox: false, + view_all_toolboxes: false, + view_requests: false, + view_services: false, + access_level_view: false, + access_level_edit: false, + manage_toolboxes: false, + debit_request_confirm: false, + refund_request_confirm: false + }; + } else { + level = editableLevels.find(l => l.id == levelId); + } + + if (!level) return; + + currentLevelId = level.id; + newLevelMode = isNew; + + const contentDiv = document.getElementById(`${modalId}-level-content`); + const isEditable = accessData.access_level_edit; + + contentDiv.innerHTML = ` +
+ + ${isNew || isEditable ? ` + + ` : ` +
${level.title}
+ `} +
+ +
+ + ${isNew || isEditable ? ` + + ` : ` +
${level.description || 'Нет описания'}
+ `} +
+ +
+
+
Основные права:
+
+ ${generateCheckbox('tools_creation', 'Создание инструментов', level.tools_creation)} + ${generateCheckbox('tools_edit', 'Редактирование инструментов', level.tools_edit)} + ${generateCheckbox('users_view', 'Просмотр пользователей', level.users_view)} + ${generateCheckbox('users_edit', 'Редактирование пользователей', level.users_edit)} + ${generateCheckbox('users_disabling', 'Блокировка пользователей', level.users_disabling)} + ${generateCheckbox('users_creation', 'Создание пользователей', level.users_creation)} + ${generateCheckbox('access_level_view', 'Просмотр уровней доступа', level.access_level_view)} + ${generateCheckbox('access_level_edit', 'Редактирование уровней доступа', level.access_level_edit)} +
+
+ +
+
Дополнительные права:
+
+ ${generateCheckbox('view_requests', 'Просмотр журнала движения', level.view_requests)} + ${generateCheckbox('view_services', 'Просмотр сервисного журнала', level.view_services)} + ${generateCheckbox('manage_toolboxes', 'Управление складами', level.manage_toolboxes)} + ${generateCheckbox('available_own_toolbox', 'Есть собственный склад', level.available_own_toolbox)} + ${generateCheckbox('tools_registration', 'Оприходование инструментов', level.tools_registration)} + ${generateCheckbox('tools_delete', 'Удаление инструментов', level.tools_delete)} + ${generateCheckbox('debit_request_confirm', 'Решение по списанию', level.debit_request_confirm)} + ${generateCheckbox('refund_request_confirm', 'Решение по возврату', level.refund_request_confirm)} + ${generateCheckbox('view_all_toolboxes', 'Просмотр всех складов', level.view_all_toolboxes)} +
+
+
+ `; + + // Вспомогательная функция для генерации чекбоксов + function generateCheckbox(name, label, checked) { + return ` +
+
+ + +
+
+ `; + } + + // Показываем/скрываем кнопки сохранения + document.getElementById(`${modalId}-save-btn`).style.display = (isEditable && !isNew) ? 'block' : 'none'; + document.getElementById(`${modalId}-add-save-btn`).style.display = (isEditable && isNew) ? 'block' : 'none'; + } + + // Обработчики для вкладок уровней + document.querySelectorAll(`.level-tab-btn`).forEach((btn, index) => { + btn.addEventListener('click', () => { + // Обновляем активную вкладку + document.querySelectorAll(`.level-tab-btn`).forEach(b => { + b.classList.remove('btn-primary'); + b.classList.add('btn-outline-primary'); + }); + btn.classList.remove('btn-outline-primary'); + btn.classList.add('btn-primary'); + + // Загружаем содержимое уровня + const levelId = btn.dataset.levelId; + newLevelMode = false; + loadLevelContent(levelId, false); + }); + }); + + // Обработчик для добавления нового уровня + if (accessData.access_level_edit) { + document.getElementById(`${modalId}-add-level-btn`).addEventListener('click', () => { + // Создаем новую вкладку + const tabsContainer = document.getElementById(`${modalId}-level-tabs`); + const newTabId = 'new-' + Date.now(); + + // Обновляем все вкладки + document.querySelectorAll(`.level-tab-btn`).forEach(b => { + b.classList.remove('btn-primary'); + b.classList.add('btn-outline-primary'); + }); + + // Добавляем новую вкладку + const newTab = document.createElement('button'); + newTab.className = 'btn btn-primary level-tab-btn'; + newTab.textContent = 'Новый уровень'; + newTab.dataset.levelId = newTabId; + newTab.addEventListener('click', () => { + document.querySelectorAll(`.level-tab-btn`).forEach(b => { + b.classList.remove('btn-primary'); + b.classList.add('btn-outline-primary'); + }); + newTab.classList.remove('btn-outline-primary'); + newTab.classList.add('btn-primary'); + loadLevelContent(newTabId, true); + }); + + // Убираем кнопку добавления + document.getElementById(`${modalId}-add-level-btn`).style.display = 'none'; + + tabsContainer.appendChild(newTab); + loadLevelContent(newTabId, true); + }); + } + + async function sendLevelData(isNew) { + const level = editableLevels.find(l => l.id == currentLevelId); + if (!level && !isNew) return; + + // Собираем данные из формы + const levelData = { + title: document.getElementById(`${modalId}-level-title`).value, + description: document.getElementById(`${modalId}-level-description`).value + }; + + if (!levelData.title.trim() || (!isNew && levelData.title !== level.title && accessLevelTitles.includes(levelData.title)) || (isNew && accessLevelTitles.includes(levelData.title))) { + showInfo('Ошибка при сохранении', 'error'); + showInfo('Введите уникальное название уровня доступа', 'warning'); + return; + } + + if (!levelData.description.trim()) { + showInfo('Ошибка при сохранении', 'error'); + showInfo('Введите описание уровня доступа', 'warning'); + return; + } + + if (!isNew) { + levelData.id = level.id; + } + + // Проверяем обязательные поля и уникальность + if (isNew) { + if (!levelData.title.trim() || editableLevels.includes(levelData.title)) { + showInfo('Ошибка при добавлении', 'error'); + showInfo('Введите уникальное название уровня доступа', 'warning'); + return; + } + } else { + if (levelData.title !== level.title && (!levelData.title.trim() || editableLevels.includes(levelData.title))) { + showInfo('Ошибка при сохранении', 'error'); + showInfo('Введите уникальное название уровня доступа', 'warning'); + return; + } + } + + // Собираем все чекбоксы + const checkboxNames = [ + 'tools_creation', 'tools_registration', 'tools_edit', 'tools_delete', + 'users_creation', 'users_edit', 'users_disabling', 'users_view', + 'available_own_toolbox', 'view_all_toolboxes', 'view_requests', + 'view_services', 'access_level_view', 'access_level_edit', + 'manage_toolboxes', 'debit_request_confirm', 'refund_request_confirm' + ]; + + checkboxNames.forEach(name => { + levelData[name] = document.getElementById(`${modalId}-${name}`).checked; + }); + + let changedLevelData = {}; + if (isNew) { + changedLevelData = levelData; + } else { + changedLevelData = Object.keys(levelData).reduce((acc, key) => { + if (levelData[key] !== level[key]) { + acc[key] = levelData[key]; + } + return acc; + }, {}); + } + + if (Object.keys(changedLevelData).length === 0) { + showInfo('Нет изменений', 'info'); + return; + } + if (!isNew) { + changedLevelData.id = level.id; + } + + const result = await apiRequest('/user/level', { action: isNew ? 'create' : 'update', changedLevelData, userId: currentUserId }); + + if (result && result.status === 'ok') { + modal.hide(); + showInfo('Данные сохранены', 'success'); + // Обновляем данные + await refreshThisTab(); + } else { + const errorMsg = result?.message || 'Произошла ошибка при сохранении'; + showInfo(errorMsg, 'error'); + } + } + + // Обработчик для сохранения изменений существующего уровня + if (accessData.access_level_edit) { + document.getElementById(`${modalId}-save-btn`).addEventListener('click', async () => { + await sendLevelData(newLevelMode); + }); + + // Обработчик для добавления нового уровня + document.getElementById(`${modalId}-add-save-btn`).addEventListener('click', async () => { + await sendLevelData(newLevelMode); + }); + } + + // Загружаем первый уровень по умолчанию + if (editableLevels.length > 0) { + loadLevelContent(editableLevels[0].id); + } + + modal.show(); + + // Удаляем модалку после скрытия + modal._element.addEventListener('hidden.bs.modal', () => { + modal._element.remove(); + }); + } + + // Обработчики для кнопок в опциональном блоке + if (accessData.users_creation) { + document.getElementById(`${tabId}-add-user-btn`).addEventListener('click', () => { + openEditUserModal(); + }); + } + + if (accessData.access_level_view) { + document.getElementById(`${tabId}-access-levels-btn`).addEventListener('click', () => { + openAccessLevelsModal(); + }); + } + + // Первоначальный рендеринг карточек + renderUsersCards(); +} + document.addEventListener('DOMContentLoaded', async () => { await getCookieData(); diff --git a/db/handlers/__pycache__/access.cpython-313.pyc b/db/handlers/__pycache__/access.cpython-313.pyc index 96388f2..003b3c0 100644 Binary files a/db/handlers/__pycache__/access.cpython-313.pyc and b/db/handlers/__pycache__/access.cpython-313.pyc differ diff --git a/db/handlers/__pycache__/records.cpython-313.pyc b/db/handlers/__pycache__/records.cpython-313.pyc index efdd21b..5fd7b10 100644 Binary files a/db/handlers/__pycache__/records.cpython-313.pyc and b/db/handlers/__pycache__/records.cpython-313.pyc differ diff --git a/db/handlers/__pycache__/user.cpython-313.pyc b/db/handlers/__pycache__/user.cpython-313.pyc index a59aea7..3287e26 100644 Binary files a/db/handlers/__pycache__/user.cpython-313.pyc and b/db/handlers/__pycache__/user.cpython-313.pyc differ diff --git a/db/handlers/access.py b/db/handlers/access.py index da16f38..b43d36b 100644 --- a/db/handlers/access.py +++ b/db/handlers/access.py @@ -6,21 +6,20 @@ from db.handlers.records import ServiceRecordsHandler class AccessLevelHandler: - async def add(newData): + async def add(newData, userId: int = None): title = newData.get("title", None) if not title: logger.error("Не указано название уровня доступа") - return {} + return {"error": "Не указано название уровня доступа"} exists = await CRUD.read(select(AccessLevel).where(AccessLevel.title == title)) if exists: logger.error("Уровень доступа с таким названием уже существует") - return {} + return {"error": "Уровень доступа с таким названием уже существует"} try: logger.info(f"Создание уровня доступа {title}") - user_id = newData.pop("user_id", None) accessData = await AccessLevel(**newData).save() await ServiceRecordsHandler.add( - user_id, {"Добавлен уровень доступа": accessData.toDict()} + userId, {"Добавлен уровень доступа": accessData.toDict()} ) except Exception as e: logger.error(f"Ошибка создания уровня доступа: {str(e)}") @@ -36,23 +35,22 @@ class AccessLevelHandler: return {} return accessData.toDict() - async def edit(accessId: int, **kwargs): - query = select(AccessLevel).where(AccessLevel.id == accessId) + async def edit(levelData, userId: int = None): + query = select(AccessLevel).where(AccessLevel.id == levelData.pop("id")) accessData = await CRUD.read(query) if not accessData: logger.error("Уровень доступа не найден") - return {} + return {"error": "Уровень доступа не найден"} try: - user_id = kwargs.pop("user_id", None) - editedAccessData = await accessData.edit(**kwargs) + editedAccessData = await accessData.edit(**levelData) await ServiceRecordsHandler.add( - user_id, {"Обновлен уровень доступа": editedAccessData.toDict()} + userId, {"Обновлен уровень доступа": editedAccessData.toDict()} ) except Exception as e: logger.error(f"Ошибка обновления уровня доступа: {str(e)}") - return {} + return {"error": "Ошибка обновления уровня доступа"} logger.info( - f"Уровень доступа {editedAccessData.title} успешно обновлен, изменены данные: {kwargs.keys()}" + f"Уровень доступа {editedAccessData.title} успешно обновлен, изменены данные: {levelData.keys()}" ) return editedAccessData.toDict() diff --git a/db/handlers/records.py b/db/handlers/records.py index 190836f..b366292 100644 --- a/db/handlers/records.py +++ b/db/handlers/records.py @@ -1,6 +1,6 @@ from datetime import date, datetime, time, timedelta -from sqlalchemy import select +from sqlalchemy import func, select from db.handlers.stock import StockHandler from db.schemas.records import StocksRecords, ServicesRecords @@ -175,6 +175,21 @@ class StocksRecordsHandler: logger.exception("Ошибка получения записей") return [] + async def getUserRecords(user_id: int) -> int: + from db import CRUD + + try: + query = select(func.count(StocksRecords.id)).where( + StocksRecords.init_user_id == user_id, + ) + logger.debug(f"Получение всех записей пользователя {user_id}") + records = await CRUD.read(query) + logger.debug(f"{records} записей пользователя {user_id} успешно получены") + return records + except Exception as e: + logger.error(f"Ошибка получения записей: {str(e)}") + return 0 + async def get(user_id: int, manager: bool): from db import CRUD diff --git a/db/handlers/user.py b/db/handlers/user.py index f4df3c2..88b2855 100644 --- a/db/handlers/user.py +++ b/db/handlers/user.py @@ -4,13 +4,20 @@ from db.handlers.access import AccessLevelHandler from db.handlers.toolbox import ToolboxHandler from utils import logger, pwd_hash, saveImage, safeFilename, deleteImage, pwd_verify from db.schemas.user import User -from db.handlers.records import ServiceRecordsHandler +from db.handlers.records import ServiceRecordsHandler, StocksRecordsHandler def handleUserPhoto(imageData, login: str): + import base64 + login = safeFilename(login) - fileName = f"users/{login}.png" - if not saveImage(imageData, fileName): + fileName = f"static/images/users/{login}.png" + 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 fileName @@ -39,9 +46,9 @@ class UserHandler: userData["hashed_password"] = pwd_hash(userData.pop("password")) if "photo" in userData: imageData = userData.pop("photo") - photoFile = handleUserPhoto(imageData, login) - if photoFile: - userData["photo"] = photoFile + imageFileName = handleUserPhoto(imageData, login) + if imageFileName: + userData["photo"] = imageFileName try: newUser = await User(**userData).save() except Exception as e: @@ -67,10 +74,9 @@ class UserHandler: logger.info( f"Тулбокс {newToolbox['title']} успешно создан и закреплен за пользователем {newUser.username}" ) - await ServiceRecordsHandler.add( - user_id, {"Добавлен пользователь": newUser.toDict()} - ) newUserData = newUser.toDict() + newUserData.pop("hashed_password") + await ServiceRecordsHandler.add(user_id, {"Добавлен пользователь": newUserData}) newUserData["access_level_data"] = userAccessLevel return newUserData @@ -78,50 +84,66 @@ class UserHandler: id = userData.get("id", None) if not id: logger.error("Не указан id пользователя") - return {} + return {"error": "Не указан id пользователя"} query = select(User).where(User.id == id) user = await CRUD.read(query) if not user: logger.error("Пользователь с таким id не найден") - return {} - changedUserData = userData.get("changedUserData", {}) - if len(changedUserData.keys()) == 0: + return {"error": "Пользователь не найден"} + if len(userData.keys()) == 0: logger.error("Не указаны изменяемые данные") - return {} - if "password" in changedUserData: - userData["hashed_password"] = pwd_hash(changedUserData.pop("password")) - if "photo" in changedUserData: - imageData = changedUserData.pop("photo") - photoFile = handleUserPhoto(imageData, user.login) - if photoFile: - changedUserData["photo"] = photoFile + return {"error": "Не указаны изменяемые данные"} + if "password" in userData: + userData["hashed_password"] = pwd_hash(userData.pop("password")) + if "photo" in userData: + imageData = userData.pop("photo") + if imageData != "": + login = user.login if "login" not in userData else userData["login"] + photoFile = handleUserPhoto(imageData, login) + if photoFile: + userData["photo"] = photoFile + deleteImage(user.photo) + else: + logger.error("Ошибка обновления фото пользователя") + return {"error": "Ошибка обновления фото пользователя"} + else: + userData["photo"] = "static/images/users/default.png" deleteImage(user.photo) try: - editedUser = await user.edit(**changedUserData) + userData.pop("id") + editedUser = await user.edit(**userData) except Exception as e: logger.error(f"Ошибка обновления пользователя: {str(e)}") - return {} + return {"error": "Ошибка обновления пользователя"} if not editedUser: logger.error("Ошибка обновления пользователя") - return {} + return {"error": "Ошибка обновления пользователя"} logger.info( - f"Пользователь {editedUser.username} успешно обновлен, изменены данные: {changedUserData.keys()}" + f"Пользователь {editedUser.username} успешно обновлен, изменены данные: {userData.keys()}" ) - if not user.available_own_toolbox: - if editedUser.available_own_toolbox: - newToolboxData = { - "title": editedUser.username, - "description": f"Оборудование, полученное сотрудником '{editedUser.username}' под личную материальную ответственность", - "owner_id": editedUser.id, - } - newToolbox = await ToolboxHandler.addNewToolbox(newToolboxData) - logger.info( - f"Тулбокс {newToolbox['title']} успешно создан и закреплен за пользователем {editedUser.username}" - ) - await ServiceRecordsHandler.add( - user_id, {"Изменен пользователь": editedUser.toDict()} - ) - return editedUser.toDict() + if user.access_level_id != editedUser.access_level_id: + userAccessLevel = await AccessLevelHandler.get(user.access_level_id) + userAccessLevelNew = await AccessLevelHandler.get( + editedUser.access_level_id + ) + if not userAccessLevel or not userAccessLevelNew: + logger.error("Уровень доступа не найден") + return {"error": "Уровень доступа не найден"} + if not userAccessLevel.get("available_own_toolbox"): + if userAccessLevelNew.get("available_own_toolbox"): + newToolboxData = { + "title": editedUser.username, + "description": f"Оборудование, полученное сотрудником '{editedUser.username}' под личную материальную ответственность", + "owner_id": editedUser.id, + } + newToolbox = await ToolboxHandler.addNewToolbox(newToolboxData) + logger.info( + f"Тулбокс {newToolbox['title']} успешно создан и закреплен за пользователем {editedUser.username}" + ) + editUserData = editedUser.toDict() + editUserData.pop("hashed_password") + await ServiceRecordsHandler.add(user_id, {"Изменен пользователь": editUserData}) + return editUserData async def getAll() -> list[dict]: query = select(User) @@ -136,23 +158,30 @@ class UserHandler: return {} return user.toDict() - async def delete(id: int, user_id: int = None) -> bool: + async def delete(id: int, user_id: int = None) -> dict: + userRecordsCount = await StocksRecordsHandler.getUserRecords(id) + if userRecordsCount > 0: + logger.error(f"У пользователя {id} есть записи: {userRecordsCount}") + return {"error": "У пользователя есть записи"} query = select(User).where(User.id == id) user = await CRUD.read(query) if not user: logger.error("Пользователь с таким id не найден") - return False + return {"error": "Пользователь не найден"} try: userName = user.username + photoFile = user.photo result = await CRUD.delete(user) except Exception as e: logger.error(f"Ошибка удаления пользователя: {str(e)}") - return False + return {"error": "Ошибка удаления пользователя"} + if result: + deleteImage(photoFile) logger.info( f"Пользователь {userName} {'успешно удален' if result else 'не удален'}" ) await ServiceRecordsHandler.add(user_id, {"Удален пользователь": userName}) - return result + return {"error": "Ошибка удаления пользователя"} if not result else {} async def deletePhoto(id: int, user_id: int = None) -> bool: query = select(User).where(User.id == id) @@ -205,25 +234,25 @@ class UserHandler: baseUsers = { "admin": { "login": "admin", - "username": "Администратор - Demo", + "username": "Администратор", "password": password, "access_level_id": acessLevels["Администратор"], }, "manager": { "login": "manager", - "username": "Менеджер - Demo", + "username": "Менеджер", "password": password, "access_level_id": acessLevels["Менеджер"], }, "storekeeper": { "login": "storekeeper", - "username": "Кладовщик - Demo", + "username": "Кладовщик", "password": password, "access_level_id": acessLevels["Кладовщик"], }, "employee": { "login": "employee", - "username": "Сотрудник - Demo", + "username": "Сотрудник", "password": password, "access_level_id": acessLevels["Сотрудник"], }, diff --git a/main.py b/main.py index 598c13e..5e2761f 100644 --- a/main.py +++ b/main.py @@ -23,8 +23,8 @@ async def main(): from db.initialize import DatabaseInitializer try: - force = False - reNewDB = False + force = True + reNewDB = True await DatabaseInitializer(DATABASE_URL).initialize(force, reNewDB) except Exception as e: logger.error(f"Инициализация базы завершилась ошибкой: {str(e)}", exc_info=True) diff --git a/utils/__pycache__/image.cpython-313.pyc b/utils/__pycache__/image.cpython-313.pyc index 12deb4c..8e6c328 100644 Binary files a/utils/__pycache__/image.cpython-313.pyc and b/utils/__pycache__/image.cpython-313.pyc differ diff --git a/utils/__pycache__/safe_filename.cpython-313.pyc b/utils/__pycache__/safe_filename.cpython-313.pyc index 542d9ec..440399c 100644 Binary files a/utils/__pycache__/safe_filename.cpython-313.pyc and b/utils/__pycache__/safe_filename.cpython-313.pyc differ diff --git a/utils/image.py b/utils/image.py index e7f2a55..6253956 100644 --- a/utils/image.py +++ b/utils/image.py @@ -34,6 +34,9 @@ def saveImage(file_bytes: bytes, file_name: str) -> bool: logger.debug(f"[ImageSave] Saving image to {target_path}") img.save(target_path, "PNG") + if not os.path.isfile(target_path): + logger.error(f"[ImageSave] File {target_path} not found") + return False return True except Exception as e: diff --git a/utils/safe_filename.py b/utils/safe_filename.py index 5cb4c11..78783e1 100644 --- a/utils/safe_filename.py +++ b/utils/safe_filename.py @@ -1,39 +1,73 @@ import re import time +from .loggers import logger # Простая транслитерация TRANSLIT_MAP = { - 'а': 'a', 'б': 'b', 'в': 'v', 'г': 'g', 'д': 'd', - 'е': 'e', 'ё': 'yo', 'ж': 'zh', 'з': 'z', 'и': 'i', - 'й': 'y', 'к': 'k', 'л': 'l', 'м': 'm', 'н': 'n', - 'о': 'o', 'п': 'p', 'р': 'r', 'с': 's', 'т': 't', - 'у': 'u', 'ф': 'f', 'х': 'h', 'ц': 'c', 'ч': 'ch', - 'ш': 'sh', 'щ': 'sch', 'ъ': '', 'ы': 'y', 'ь': '', - 'э': 'e', 'ю': 'yu', 'я': 'ya' + "а": "a", + "б": "b", + "в": "v", + "г": "g", + "д": "d", + "е": "e", + "ё": "yo", + "ж": "zh", + "з": "z", + "и": "i", + "й": "y", + "к": "k", + "л": "l", + "м": "m", + "н": "n", + "о": "o", + "п": "p", + "р": "r", + "с": "s", + "т": "t", + "у": "u", + "ф": "f", + "х": "h", + "ц": "c", + "ч": "ch", + "ш": "sh", + "щ": "sch", + "ъ": "", + "ы": "y", + "ь": "", + "э": "e", + "ю": "yu", + "я": "ya", } # Заглавные буквы → в ту же латиницу, но без capital (ниже мы в lower() всё равно переводим) TRANSLIT_MAP.update({k.upper(): v for k, v in TRANSLIT_MAP.items()}) + def transliterate(text: str) -> str: - return ''.join(TRANSLIT_MAP.get(ch, ch) for ch in text) + return "".join(TRANSLIT_MAP.get(ch, ch) for ch in text) + def safeFilename(name: str) -> str: - # 1. Транслитерация кириллицы - name = transliterate(name) + try: + # 1. Транслитерация кириллицы + name = transliterate(name) - # 2. Приводим к нижнему регистру - name = name.lower() + # 2. Приводим к нижнему регистру + name = name.lower() - # 3. Заменяем всё, что не буква/цифра, на "_" - name = re.sub(r'[^a-z0-9]+', '_', name) + # 3. Заменяем всё, что не буква/цифра, на "_" + name = re.sub(r"[^a-z0-9]+", "_", name) - # 4. Убираем повторяющиеся "_" - name = re.sub(r'_+', '_', name).strip('_') + # 4. Убираем повторяющиеся "_" + name = re.sub(r"_+", "_", name).strip("_") - # 5. Ограничиваем длину - name = name[:80] or "file" + # 5. Ограничиваем длину + name = name[:80] or "file" - # 6. Добавляем таймштамп - timestamp = int(time.time() * 1000) # миллисекунды - return f"{name}_{timestamp}" \ No newline at end of file + # 6. Добавляем таймштамп + timestamp = int(time.time() * 1000) # миллисекунды + return f"{name}_{timestamp}" + + except Exception as e: + logger.error(f"Ошибка создания названия файла: {str(e)}") + return "file"