Загрузка данных
# -*- coding: utf-8 -*-
# adminmanager.py
import hashlib
import os
import shutil
import sqlite3
import tkinter as tk
import uuid
from datetime import datetime
from tkinter import filedialog, messagebox, ttk
def center_on_parent(child_window):
"""
Универсальная функция для центрирования дочернего окна.
Работает корректно как при наличии parent, так и без него (None).
"""
# Обновляем геометрию окна, чтобы winfo_reqwidth/height были актуальны
child_window.update_idletasks()
# Получаем размеры и координаты ребенка
window_width = child_window.winfo_reqwidth()
window_height = child_window.winfo_reqheight()
# --- НАЧАЛО БЛОКА ИСПРАВЛЕНИЯ ---
# Проверяем, есть ли у окна родитель (мастер)
if child_window.master is not None:
# Режим 1: Родитель есть. Центрирование относительно родителя.
parent = child_window.master
# Получаем координаты и размеры родителя
x = parent.winfo_rootx() + (parent.winfo_width() // 2) - (window_width // 2)
y = parent.winfo_rooty() + (parent.winfo_height() // 2) - (window_height // 2)
else:
# Режим 2: Родителя нет. Центрирование относительно всего экрана.
screen_width = child_window.winfo_screenwidth()
screen_height = child_window.winfo_screenheight()
x = (screen_width // 2) - (window_width // 2)
y = (screen_height // 2) - (window_height // 2)
# --- КОНЕЦ БЛОКА ИСПРАВЛЕНИЯ ---
# Устанавливаем новую позицию окна
child_window.geometry(f"+{x}+{y}")
# Класс диалогового окна для добавления пользователя
class AddUserDialog(tk.Toplevel):
def __init__(self, parent):
super().__init__(parent)
self.parent = parent # Родительское окно
self.title("Добавление пользователя")
self.result = None
self.protocol("WM_DELETE_WINDOW", self.cancel)
# Логин
lbl_username = ttk.Label(self, text="Логин:")
lbl_username.grid(row=0, column=0, sticky="w", padx=10, pady=5)
self.entry_username = ttk.Entry(self)
self.entry_username.grid(row=0, column=1, padx=10, pady=5)
# Роль
lbl_role = ttk.Label(self, text="Роль:")
lbl_role.grid(row=1, column=0, sticky="w", padx=10, pady=5)
self.combobox_role = ttk.Combobox(
self, values=["Администратор", "Эксперт", "Пользователь"]
)
self.combobox_role.grid(row=1, column=1, padx=10, pady=5)
# Пароль
lbl_password = ttk.Label(self, text="Пароль:")
lbl_password.grid(row=2, column=0, sticky="w", padx=10, pady=5)
self.entry_password = ttk.Entry(self, show="*")
self.entry_password.grid(row=2, column=1, padx=10, pady=5)
# Кнопки
btn_frame = ttk.Frame(self)
btn_frame.grid(row=3, columnspan=2, pady=10)
btn_ok = ttk.Button(btn_frame, text="OK", command=self.ok)
btn_ok.pack(side="left", padx=10)
btn_cancel = ttk.Button(btn_frame, text="Отмена", command=self.cancel)
btn_cancel.pack(side="right", padx=10)
def ok(self):
username = self.entry_username.get()
role = self.combobox_role.get()
password = self.entry_password.get()
if username and role and password:
self.result = (username, role, password)
self.destroy()
else:
messagebox.showwarning("Ошибка", "Все поля обязательны для заполнения!")
def cancel(self):
self.result = None
self.destroy()
# === Окно для работы с пользователями ===
class UsersWindow(tk.Toplevel):
def __init__(self, parent, main_app_reference=None):
super().__init__(parent)
self.main_app = main_app_reference # Сохраняем ссылку
self.title("Пользователи")
self.build_ui()
center_on_parent(self)
def build_ui(self):
# Контейнер
container = ttk.Frame(self)
container.pack(fill="both", expand=True)
# Левая панель кнопок
left_panel = ttk.Frame(container)
left_panel.pack(side="left", fill="y", padx=10, pady=10)
# Кнопка добавления пользователя
btn_add_user = ttk.Button(
left_panel, text="Добавить пользователя", command=self.add_user
)
btn_add_user.pack(fill="x", pady=5)
# Кнопка смены пользователя
btn_change_user = ttk.Button(
left_panel, text="Сменить пользователя", command=self.change_user
)
btn_change_user.pack(fill="x", pady=5)
# Кнопка удаления пользователя
btn_remove_user = ttk.Button(
left_panel, text="Удалить пользователя", command=self.remove_user
)
btn_remove_user.pack(fill="x", pady=5)
# Правая панель с таблицей пользователей
right_panel = ttk.Frame(container)
right_panel.pack(side="right", fill="both", expand=True)
# Лист пользователей (Treeview)
columns = ("ID", "Логин", "Роль")
self.users_tree = ttk.Treeview(right_panel, columns=columns, show="headings")
for col in columns:
self.users_tree.heading(col, text=col)
self.users_tree.column(
col, stretch=True, anchor="w", width=100
) # Ширина столбца
self.users_tree.pack(fill="both", expand=True)
# Загрузка данных
self.load_users()
def load_users(self):
"""Загружает данные о пользователях в дерево."""
conn = sqlite3.connect("users.db")
cursor = conn.cursor()
cursor.execute("SELECT id, username, role FROM users ORDER BY id ASC;")
rows = cursor.fetchall()
# Очистка таблицы
for i in self.users_tree.get_children():
self.users_tree.delete(i)
# Добавляем новые данные без четвертого столбца
for row in rows:
self.users_tree.insert("", "end", values=(row[0], row[1], row[2]))
conn.close()
def add_user(self):
"""Метод для добавления пользователя."""
dialog = AddUserDialog(self)
self.wait_window(dialog)
if dialog.result:
username, role, password = dialog.result
# Хэшируем пароль
salt = uuid.uuid4().hex
hashed_password = hashlib.sha256((password + salt).encode()).hexdigest()
# Добавляем пользователя в базу данных
with sqlite3.connect("users.db") as conn:
cursor = conn.cursor()
cursor.execute(
"INSERT INTO users (username, role, password_hash, salt) VALUES (?,?,?,?)",
(username, role, hashed_password, salt),
)
conn.commit()
# Обновляем таблицу
self.load_users()
# Внутри класса UsersWindow в adminmanager.py
def change_user(self):
"""
Открывает диалоговое окно для смены/выбора пользователя.
Само это окно не выполняет выход, оно лишь ждет результата от диалога входа.
"""
# Создаем модальное диалоговое окно для входа
login_dialog = LoginDialog(parent=self)
# Ждем, пока диалоговое окно не закроется
self.wait_window(login_dialog)
# --- КОД НИЖЕ ВЫПОЛНИТСЯ ТОЛЬКО ПОСЛЕ ЗАКРЫТИЯ ОКНА ---
# Проверяем, был ли вход успешным (флаг устанавливается в LoginDialog)
if hasattr(login_dialog, 'login_success') and login_dialog.login_success:
messagebox.showinfo("Успех", "Пользователь сменен.")
# После успешной смены можно закрыть окно управления пользователями
self.destroy()
def remove_user(self):
"""Метод для удаления пользователя и правильного обновления последовательности ID"""
selected_item = self.users_tree.selection()
if not selected_item:
messagebox.showwarning("Ошибка", "Выберите пользователя для удаления!")
return
# Получаем ID выбранного пользователя
user_id = int(self.users_tree.item(selected_item)["values"][0])
# Подтверждение удаления
answer = messagebox.askyesno(
"Подтверждение удаления", f"Удалить пользователя с ID={user_id}?"
)
if not answer:
return
# Удаляем пользователя и перестраиваем ID
with sqlite3.connect("users.db") as conn:
cursor = conn.cursor()
# Удаляем пользователя
cursor.execute("DELETE FROM users WHERE id=?", (user_id,))
# Перестраиваем последовательность ID
cursor.execute("SELECT * FROM users ORDER BY id ASC;")
rows = cursor.fetchall()
new_id = 1
for row in rows:
old_id = row[0]
cursor.execute("UPDATE users SET id=? WHERE id=?", (new_id, old_id))
new_id += 1
# Сбрасываем счётчик AUTO_INCREMENT
cursor.execute("DELETE FROM sqlite_sequence WHERE name='users';")
conn.commit()
# Обновляем таблицу
self.load_users()
# === Окно для работы с базами данных ===
class DatabaseWindow(tk.Toplevel):
def __init__(self, parent):
super().__init__(parent)
# Убираем сохранение собственного атрибута .parent
# self.parent = parent
self.title("Управление базами данных")
self.geometry("600x400")
# --- ИСПРАВЛЕНИЕ ЗДЕСЬ ---
# Вызываем функцию центрирования только по себе.
# Функция сама найдет главного родителя через self.master
center_on_parent(self)
# Основная структура интерфейса
self.build_ui()
def build_ui(self):
# Основной контейнер
container = ttk.Frame(self)
container.pack(fill="both", expand=True)
# Верхняя панель настроек
top_panel = ttk.Frame(container)
top_panel.pack(fill="x", pady=10)
# Переключатель баз данных
self.use_master_db = tk.BooleanVar(value=False)
style = ttk.Style()
style.configure("RedText.TCheckbutton", foreground="red")
master_db_checkbox = ttk.Checkbutton(
top_panel,
text="Использовать основную базу данных",
variable=self.use_master_db,
style="RedText.TCheckbutton",
)
master_db_checkbox.pack(side="left", padx=10)
# Панель инструментов
tools_panel = ttk.Frame(container)
tools_panel.pack(fill="x", pady=10)
# Кнопка резервного копирования текущей базы
btn_current_backup = ttk.Button(
tools_panel,
text="Создать резервную копию текущей базы",
command=self.backup_current_database,
)
btn_current_backup.pack(side="left", padx=10)
# Кнопка восстановления базы данных
btn_restore = ttk.Button(
tools_panel, text="Восстановить базу данных", command=self.restore_database
)
btn_restore.pack(side="left", padx=10)
# Кнопка миграции баз данных
btn_migrate = ttk.Button(
tools_panel, text="Мигрировать данные", command=self.migrate_databases
)
btn_migrate.pack(side="left", padx=10)
# Таблица резервных копий
backup_columns = ("Дата", "Файл")
self.backups_tree = ttk.Treeview(
container, columns=backup_columns, show="headings"
)
for col in backup_columns:
self.backups_tree.heading(col, text=col)
self.backups_tree.column(col, width=200)
self.backups_tree.pack(fill="both", expand=True)
# Загружаем существующие резервные копии
self.load_backups()
def load_backups(self):
"""Загружает список существующих резервных копий в дерево."""
self.backups_tree.delete(*self.backups_tree.get_children()) # Очищаем дерево
# Директория резервных копий
backup_dir = "backups/"
os.makedirs(backup_dir, exist_ok=True)
# Список всех резервных копий
backups = []
for filename in os.listdir(backup_dir):
if filename.endswith(".db"): # Фильтруем только файлы SQLite
filepath = os.path.join(backup_dir, filename)
creation_time = datetime.fromtimestamp(
os.path.getmtime(filepath)
).strftime("%Y-%m-%d %H:%M:%S")
backups.append({"file": filename, "creation_time": creation_time})
# Сортируем резервные копии по дате создания
sorted_backups = sorted(backups, key=lambda b: b["creation_time"], reverse=True)
# Добавляем записи в TreeView
for backup in sorted_backups:
self.backups_tree.insert(
"", "end", values=(backup["creation_time"], backup["file"])
)
def backup_current_database(self):
"""Создает резервную копию текущей базы данных."""
db_filename = "database.db" if not self.use_master_db.get() else "masterdata.db"
# Полный путь к файлу резервной копии
backup_dir = "backups/"
os.makedirs(backup_dir, exist_ok=True)
now = datetime.now()
backup_filename = f"{now.strftime('%Y-%m-%d_%H-%M-%S')}__{db_filename}"
backup_filepath = os.path.join(backup_dir, backup_filename)
try:
shutil.copy(db_filename, backup_filepath)
messagebox.showinfo(
"Резервное копирование",
f"Резервная копия успешно создана:\n{backup_filepath}",
)
self.load_backups() # Обновляем список резервных копий
except Exception as e:
messagebox.showerror("Ошибка", str(e))
def restore_database(self):
"""Восстанавливает выбранную резервную копию."""
selected_item = self.backups_tree.selection()
if len(selected_item) > 0:
backup_filename = self.backups_tree.item(selected_item)[0]["values"][1]
backup_filepath = os.path.join("backups/", backup_filename)
active_db = (
"database.db" if not self.use_master_db.get() else "masterdata.db"
)
try:
shutil.copy(backup_filepath, active_db)
messagebox.showinfo(
"Восстановление",
f"База данных успешно восстановлена из резервной копии.\nАктивная база данных обновлена.",
)
self.load_backups() # Обновляем список резервных копий
except Exception as e:
messagebox.showerror("Ошибка", str(e))
else:
messagebox.showwarning(
"Предупреждение", "Сначала выберите резервную копию для восстановления."
)
def migrate_databases(self):
"""Выполняет миграцию данных между двумя базами данных."""
if not self.use_master_db.get():
src_db = "database.db"
dest_db = "masterdata.db"
else:
src_db = "masterdata.db"
dest_db = "database.db"
# Создание временной копии исходной базы данных
temp_src_db = f"{src_db}_temp"
shutil.copy(src_db, temp_src_db)
# Перемещение данных
try:
# Открытие обеих баз данных
conn_src = sqlite3.connect(temp_src_db)
conn_dest = sqlite3.connect(dest_db)
# Копирование содержимого одной базы в другую
conn_src.backup(conn_dest)
# Закрытие соединений
conn_src.close()
conn_dest.close()
# Сообщение об успехе
messagebox.showinfo("Миграция", "Данные успешно перенесены.")
except Exception as e:
messagebox.showerror("Ошибка", str(e))
class LoginDialog(tk.Toplevel):
"""Диалоговое окно для авторизации нового пользователя."""
def __init__(self, parent):
super().__init__(parent)
self.title("Вход в систему")
self.geometry("300x200")
self.resizable(False, False)
# Флаг успешной авторизации по умолчанию - False
self.successful = False
frame = ttk.Frame(self, padding="20")
frame.pack(expand=True)
# --- Поля ввода ---
ttk.Label(frame, text="Логин:").grid(row=0, column=0, sticky="e", pady=5)
self.entry_login = ttk.Entry(frame)
self.entry_login.grid(row=0, column=1, padx=10, pady=5, sticky="we")
ttk.Label(frame, text="Пароль:").grid(row=1, column=0, sticky="e", pady=5)
self.entry_pass = ttk.Entry(frame, show="*") # Символы пароля скрыты звездочками
self.entry_pass.grid(row=1, column=1, padx=10, pady=5, sticky="we")
# --- Кнопки ---
btn_frame = ttk.Frame(frame)
btn_frame.grid(row=2, column=0, columnspan=2, pady=15)
btn_ok = ttk.Button(btn_frame, text="Войти", command=self.on_login)
btn_ok.pack(side="left", expand=True, ipadx=10, ipady=5)
btn_cancel = ttk.Button(btn_frame, text="Отмена", command=self.destroy)
btn_cancel.pack(side="right", expand=True, ipadx=10, ipady=5)
def on_login(self):
"""
Проверяет введенные данные по базе данных SQLite.
"""
login = self.entry_login.get()
password = self.entry_pass.get()
# Если поля пусты - выходим сразу
if not (login and password):
messagebox.showerror("Ошибка", "Пожалуйста, заполните все поля!")
return
try:
# Подключаемся к базе данных пользователей
conn = sqlite3.connect("users.db")
cursor = conn.cursor()
# 1. Находим пользователя по логину
cursor.execute("SELECT id, username, role, password_hash, salt FROM users WHERE username=?", (login,))
user_data = cursor.fetchone()
# 2. Проверяем, нашли ли мы такого пользователя
if user_data is None:
raise ValueError("Пользователь не найден.")
# Достаём из базы хеш пароля и соль
db_password_hash = user_data[3]
salt = user_data[4]
# 3. Хешируем введенный пароль с той же солью
hashed_input = hashlib.sha256((password + salt).encode()).hexdigest()
# 4. Сравниваем полученный хеш с тем, что лежит в базе
if hashed_input == db_password_hash:
# --- УСПЕХ ---
messagebox.showinfo("Успех", f"Вход выполнен успешно!\nПриветствую, {user_data[1]}!")
# Здесь можно сохранить данные о пользователе куда нужно
# Например, передать их в главное окно через атрибут
self.selected_user = {
"username": user_data[1],
"role": user_data[2]
}
self.successful = True
self.destroy() # Закрываем окно
else:
# Пароль неверный
raise ValueError("Неверный логин или пароль.")
except Exception as e:
# Сюда попадём, если пользователь не найден или пароль неверный
print(f"Ошибка входа:", e) # Для отладки
messagebox.showerror("Ошибка", "Неверный логин или пароль.")
finally:
# Обязательно закрываем соединение с БД
if 'conn' in locals():
conn.close()