Загрузка данных
# -*- 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(parent, child):
"""
Функция для размещения дочернего окна по центру родительского окна.
"""
x = parent.winfo_x() + (parent.winfo_width() // 2) - (child.winfo_reqwidth() // 2)
y = parent.winfo_y() + (parent.winfo_height() // 2) - (child.winfo_reqheight() // 2)
child.geometry("+{}+{}".format(x, y))
# Класс диалогового окна для добавления пользователя
class AddUserDialog(tk.Toplevel):
def __init__(self, parent):
super().__init__(parent)
self.parent = parent # Родительское окно
self.title("Добавление пользователя")
# Логин
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):
super().__init__(parent)
self.parent = parent # Родительское окно
self.title("Пользователи")
# Основное содержимое окна
self.build_ui()
# Центрирование окна на экране родителя
center_on_parent(self.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()
def change_user(self):
"""Метод для полного выхода из системы и возвращения в форму авторизации."""
self.destroy()
self.parent.logout()
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)
self.parent = parent # Родительское окно
self.title("Управление базами данных")
self.geometry("600x400")
# Центрируем окно на экране родителя
center_on_parent(self.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))