Загрузка данных
# -*- coding: utf-8 -*-
# gui/indicator_manager.py
import os
import random
import tkinter as tk
from tkinter import filedialog, messagebox, ttk
from fpdf import FPDF
from sqlalchemy import Integer, create_engine, func
from sqlalchemy.orm import sessionmaker
# Импортируем из модуля базы данных
from modules.database_module import Indicator
from modules.database_module import Session as DBSession
from modules.database_module import (
delete_indicator_by_code,
get_all_indicators_by_group,
insert_indicator,
list_groups,
)
DEBUG_LOGGING = False
class IndicatorManager(tk.Toplevel):
def __init__(self, parent):
super().__init__(parent)
self.parent = parent
self.title("Менеджер показателей")
self.geometry("600x450")
# --- Инициализация и доступ к базе данных ---
if not hasattr(parent, "current_user"):
raise AttributeError("Ошибка: Родительское окно не авторизовано.")
if not self.check_role_access(parent):
messagebox.showwarning(
"Доступ запрещён", "Нужен статус администратора или эксперта."
)
self.destroy()
return
self.use_master_db = self.parent.is_master_db_active
# Создаем движок (engine) и сессию ОДИН РАЗ при создании окна
db_path = "masterdata.db" if self.use_master_db.get() else "database.db"
self.engine = create_engine(f"sqlite:///{db_path}", echo=False)
# <-- ЕДИНАЯ СЕССИЯ для всего окна
self.session = DBSession(bind=self.engine)
# Привязываем закрытие окна к методу закрытия сессии
self.protocol("WM_DELETE_WINDOW", self.on_close)
# Словарь для связи названий групп с их кодами
self.group_names_to_codes = {}
# --- Создание интерфейса ---
self.create_widgets()
# Первоначальное заполнение данных
self.update_group_combobox()
self.update_indicators_list()
def create_widgets(self):
"""Метод для создания всех виджетов интерфейса."""
top_panel = tk.Frame(self)
top_panel.pack(padx=10, pady=10, fill="x")
# Выбор группы
tk.Label(top_panel, text="Группа:").pack(anchor="w")
self.group_var = tk.StringVar()
self.group_combobox = ttk.Combobox(
top_panel, textvariable=self.group_var, state="readonly"
)
self.group_combobox.pack(fill="x", pady=5)
self.group_combobox.bind("<<ComboboxSelected>>", self.update_indicators_list)
# Поле ввода названия
tk.Label(top_panel, text="Название показателя:").pack(anchor="w")
self.name_entry = tk.Text(top_panel, height=3, width=40)
self.name_entry.pack(fill="x", pady=5)
# Кнопка вставки из буфера (упрощенная логика)
paste_button = tk.Button(
top_panel,
text="Вставить",
command=lambda: self.name_entry.insert(tk.END, self.clipboard_get()),
)
paste_button.pack(anchor="e")
# Кнопки действий
buttons_frame = tk.Frame(top_panel)
buttons_frame.pack(fill="x", pady=5)
add_btn = tk.Button(buttons_frame, text="Добавить", command=self.add_indicator)
add_btn.pack(side="left", padx=2)
del_btn = tk.Button(
buttons_frame, text="Удалить", command=self.delete_indicator
)
del_btn.pack(side="left", padx=2)
print_btn = tk.Button(
buttons_frame, text="Печать PDF", command=self.print_to_pdf
)
print_btn.pack(side="left", padx=2)
# Список показателей со скроллбаром
middle_panel = tk.Frame(self)
middle_panel.pack(fill="both", expand=True, padx=10, pady=5)
scrollbar = tk.Scrollbar(middle_panel)
scrollbar.pack(side="right", fill="y")
self.indicators_list = tk.Text(
middle_panel,
wrap="word",
yscrollcommand=scrollbar.set,
font=("Arial", 10),
state="disabled",
)
self.indicators_list.pack(fill="both", expand=True)
scrollbar.config(command=self.indicators_list.yview)
# Биндинг событий для списка
self.indicators_list.bind("<Double-Button-1>", self.edit_indicator)
def on_close(self):
"""Корректно закрывает сессию и окно."""
try:
self.session.close()
print("Сессия базы данных закрыта.")
except Exception as e:
print(f"Ошибка при закрытии сессии: {e}")
finally:
self.destroy()
def add_indicator(self):
group_name = self.group_var.get()
group_code = self.group_names_to_codes.get(group_name)
indicator_name = self.name_entry.get("1.0", tk.END).rstrip("\n").strip()
if not group_code or not indicator_name:
messagebox.showwarning("Ошибка", "Выберите группу и введите название!")
return
new_code = self.generate_unique_code(group_code)
new_indicator = Indicator(
code=new_code, name=indicator_name, group_code=group_code
)
self.session.add(new_indicator)
self.session.commit() # Сохраняем в главной сессии
self.name_entry.delete("1.0", tk.END)
self.update_indicators_list()
def generate_unique_code(self, group_code):
"""Генерирует гарантированно уникальный код."""
index = 1
# Цикл будет выполняться, пока не найдется свободный код
while True:
new_code = f"{group_code}-{index}"
# Проверяем в базе, есть ли запись с таким кодом
exists = (
self.session.query(Indicator.code).filter_by(code=new_code).first()
is not None
)
if not exists:
return new_code # Возвращаем первый же свободный код
# Если код занят, пробуем следующий номер по порядку
index += 1
def update_group_combobox(self):
"""Заполняет выпадающий список групп."""
groups = list_groups() # Предполагаем, что эта функция работает с текущей базой
self.group_names_to_codes.clear()
for group in groups:
self.group_names_to_codes[group.name] = group.code
group_names = [group.name for group in groups]
self.group_combobox["values"] = group_names
if group_names:
self.group_combobox.current(0)
self.update_indicators_list() # Обновить список при первой загрузке
def update_indicators_list(self, event=None):
"""Обновляет список индикаторов в виджете Text."""
selected_group_name = self.group_var.get()
selected_group_code = self.group_names_to_codes.get(selected_group_name)
if not selected_group_code:
return
# Используем главную сессию self.session!
indicators = (
self.session.query(Indicator)
.filter_by(group_code=selected_group_code)
.all()
)
self.indicators_list.configure(state="normal")
self.indicators_list.delete("1.0", tk.END)
for i, ind in enumerate(indicators):
self.indicators_list.insert(tk.END, f"{i + 1}. {ind.name}\n")
self.indicators_list.insert(tk.END, f"Код: {ind.code}\n\n")
self.indicators_list.configure(state="disabled")
def edit_indicator(self, event):
"""Открывает диалог для редактирования выбранного индикатора."""
try:
cursor_pos = self.indicators_list.index(tk.INSERT)
line_num = int(cursor_pos.split(".")[0])
# Вычисляем индекс выбранного элемента (3 строки на элемент:
# название, код, пустая строка)
index = (line_num - 1) // 3
# Получаем текст из виджета и парсим его
text_content = self.indicators_list.get("1.0", "end-1c")
lines = text_content.split("\n")
name_line = lines[index * 3]
code_line = lines[index * 3 + 1]
name = name_line.split(". ", 1)[1]
code = code_line.split(": ", 1)[1]
edit_win = tk.Toplevel(self)
edit_win.title("Редактировать")
edit_win.geometry("350x150")
tk.Label(edit_win, text="Новое название:").pack(pady=5)
new_name_entry = tk.Text(edit_win, height=2, width=40)
new_name_entry.insert(tk.END, name)
new_name_entry.pack(pady=5)
save_btn = tk.Button(
edit_win,
text="Сохранить",
command=lambda: self.save_changes(
code, new_name_entry.get("1.0", tk.END), edit_win
),
)
save_btn.pack(pady=5)
except (IndexError, ValueError):
pass # Кликнули не по элементу списка
def save_changes(self, code, new_name_raw, edit_win):
"""Сохраняет изменения в базу данных."""
new_name = new_name_raw.rstrip("\n").strip()
if not new_name:
messagebox.showwarning("Ошибка", "Название не может быть пустым.")
return
# Используем главную сессию!
indicator = self.session.query(Indicator).filter_by(code=code).first()
if indicator:
indicator.name = new_name
self.session.commit() # Сохраняем в главной сессии
self.update_indicators_list()
messagebox.showinfo("Готово", "Изменения сохранены.")
edit_win.destroy()
else:
messagebox.showwarning("Ошибка", "Индикатор не найден.")
def delete_indicator(self):
try:
cursor_pos = self.indicators_list.index(tk.INSERT)
line_num = int(cursor_pos.split(".")[0])
index = (line_num - 1) // 3
text_content = self.indicators_list.get("1.0", "end-1c")
lines = text_content.split("\n")
code_line = lines[index * 3 + 1]
code = code_line.split(": ", 1)[1]
if messagebox.askyesno(
"Подтверждение", f"Удалить индикатор с кодом {code}?"
):
success = delete_indicator_by_code(code)
if success:
messagebox.showinfo("Готово", "Удалено успешно.")
self.update_indicators_list()
else:
messagebox.showerror("Ошибка", "Не удалось удалить.")
# Исправлено: отступ такой же, как у 'try'
except (IndexError, ValueError):
messagebox.showwarning("Ошибка", "Выберите индикатор для удаления.")
def print_to_pdf(self):
"""Экспортирует список индикаторов в PDF."""
group_name = self.group_var.get()
if not group_name:
messagebox.showwarning("Ошибка", "Выберите группу.")
return
group_code = self.group_names_to_codes.get(group_name)
indicators = (
self.session.query(Indicator).filter_by(group_code=group_code).all()
)
if not indicators:
messagebox.showwarning("Ошибка", "В группе нет показателей.")
return
pdf = FPDF()
try:
pdf.add_font("DejaVuSans", "", "DejaVuSans.ttf", uni=True)
except BaseException:
pass # Шрифт может быть уже добавлен или отсутствовать
pdf.add_page()
pdf.set_font("DejaVuSans", size=12)
pdf.cell(0, 10, f"Группа: {group_name}", ln=True, align="C")
pdf.ln(5)
for idx, ind in enumerate(indicators):
pdf.cell(0, 8, f"{idx + 1}. {ind.name}", ln=True)
pdf.cell(0, 8, f"Код: {ind.code}", ln=True)
pdf.ln(4)
filename = f"{group_name.replace(' ', '_')}_indicators.pdf"
path = filedialog.asksaveasfilename(
initialfile=filename,
defaultextension=".pdf",
filetypes=[("PDF files", "*.pdf"), ("All files", "*.*")],
)
if path:
try:
pdf.output(path)
messagebox.showinfo(
"Готово", f"Файл сохранен как:\n{os.path.basename(path)}"
)
except Exception as e:
messagebox.showerror("Ошибка сохранения", str(e))
def check_role_access(self, parent):
user_roles = {"Администратор", "Эксперт"}
current_user = getattr(parent, "current_user", {})
role = current_user.get("role")
return bool(role) and role in user_roles
if __name__ == "__main__":
root = tk.Tk()
# Для теста создаем фиктивного родителя с нужными атрибутами
class FakeParent:
def __init__(self):
self.current_user = {"role": "Администратор"}
self.is_master_db_active = tk.BooleanVar(value=False)
fake_parent = FakeParent()
manager = IndicatorManager(fake_parent)
root.mainloop()