Загрузка данных
# Файл-донор. Был предназначен для создания Эксель файлов для Экспертов.
import os
import tkinter as tk
from tkinter import messagebox, ttk
# ------------------------------------
import pandas as pd
from openpyxl import Workbook
from openpyxl.utils.dataframe import dataframe_to_rows
# --- ИСПРАВЛЕННЫЙ ИМПОРТ ---
# Теперь мы импортируем обе необходимые функции из базы данных
from modules.database_module import list_groups, list_indicators
def create_excel_template(groups_codes, output_filename):
"""
Функция создаёт шаблон Excel-файла для заданных групп и сохраняет его.
Все необходимые данные подгружаются за один проход без повторных запросов.
"""
wb = Workbook()
# Загружаем группы и их индикаторы со связями EAGER loading,
# чтобы избежать обращения к БД при обращении к .group или .matrices
all_groups_with_data = list_groups()
for group_code in groups_codes:
try:
# Ищем группу в уже загруженном списке
group = next((g for g in all_groups_with_data if g.code == group_code), None)
# Проверяем наличие группы и её индикаторов
indicators_list = getattr(group, 'indicators', []) if group else []
if not indicators_list:
continue
sheet_title = f"{group.name}"
ws = wb.create_sheet(title=sheet_title)
# --- ЗАГОЛОВКИ ТАБЛИЦЫ ---
ws.cell(row=1, column=1, value="№ п/п")
ws.cell(row=1, column=2, value="Показатель")
ws.cell(row=1, column=3, value="Код")
# --- ЗАПОЛНЯЕМ ТАБЛИЦУ ПОКАЗАТЕЛЕЙ ---
for idx, ind in enumerate(indicators_list):
ws.cell(row=idx + 2, column=1, value=str(idx + 1))
ws.cell(row=idx + 2, column=2, value=ind.name)
ws.cell(row=idx + 2, column=3, value=ind.code)
# --- ПОДГОТОВКА К СОЗДАНИЮ МАТРИЦЫ ---
start_col = 4
matrix_start_row = 1
for col_idx, label in enumerate([ind.code for ind in indicators_list]):
ws.cell(row=matrix_start_row, column=start_col + col_idx, value=label)
matrix_size = len(indicators_list)
# --- СОЗДАЕМ СЛОВАРЬ ДЛЯ БЫСТРОГО ПОИСКА ЗНАЧЕНИЙ ИЗ БАЗЫ ---
matrix_data_dict = {}
if group and hasattr(group, 'indicators') and group.indicators:
for ind in group.indicators: # Проходим по индикаторам конкретной группы
if hasattr(ind, 'matrices'):
for entry in ind.matrices: # Берём матрицы конкретного индикатора
matrix_data_dict[(entry.row, entry.col)] = entry.value
# --- ЗАПОЛНЯЕМ ЯЧЕЙКИ МАТРИЦЫ ДАННЫМИ ИЗ БАЗЫ ---
for r_idx in range(matrix_size):
for c_idx in range(matrix_size):
db_value = matrix_data_dict.get((r_idx, c_idx))
target_cell = ws.cell(
row=matrix_start_row + r_idx + 1,
column=start_col + c_idx
)
if db_value is not None:
target_cell.value = float(db_value)
elif r_idx == c_idx:
target_cell.value = 1.0
else:
target_cell.value = ""
except Exception as e:
print(f"Ошибка при обработке группы '{group_code}': {e}")
if "Sheet" in wb.sheetnames:
del wb["Sheet"]
wb.save(output_filename)
class ExportMatrixWindow(tk.Toplevel):
def __init__(self, parent):
super().__init__(parent)
self.title("Создание файла для эксперта")
self.geometry("450x200")
main_frame = ttk.Frame(self)
main_frame.pack(padx=10, pady=10, fill="both", expand=True)
self.groups = []
self.selected_groups = []
self.num_experts = tk.IntVar()
tk.Label(main_frame, text="Группа:").grid(row=0, column=0, sticky="w")
self.group_var = tk.StringVar()
self.combobox = ttk.Combobox(
main_frame, textvariable=self.group_var, state="readonly"
)
self.combobox.grid(row=0, column=1, padx=(5, 10), sticky="ew")
self.refresh_groups()
add_group_btn = ttk.Button(
main_frame, text="Добавить группу", command=self.add_group
)
add_group_btn.grid(row=0, column=2, padx=(0, 10), sticky="e")
tk.Label(main_frame, text="Выбранные группы:").grid(row=1, column=0, sticky="w")
self.selected_groups_text = tk.Text(main_frame, width=30, height=4)
self.selected_groups_text.grid(
row=1, column=1, columnspan=2, pady=(5, 10), sticky="ew"
)
tk.Label(main_frame, text="Количество экспертов:").grid(
row=2, column=0, sticky="w"
)
self.experts_count_entry = ttk.Entry(main_frame, textvariable=self.num_experts)
self.experts_count_entry.grid(row=2, column=1, sticky="w")
remove_group_btn = ttk.Button(
main_frame, text="Удалить группу", command=self.remove_group
)
remove_group_btn.grid(row=2, column=2, sticky="e")
export_btn = ttk.Button(
main_frame, text="Экспортировать в Excel", command=self.export_to_excel
)
export_btn.grid(row=3, column=1, pady=(10, 0))
def refresh_groups(self):
self.groups = [grp.name for grp in list_groups()]
self.combobox.config(values=self.groups)
def add_group(self):
group_name = self.group_var.get()
if group_name and group_name.strip():
self.selected_groups.append(group_name)
self.selected_groups_text.insert(tk.END, f"{group_name}\n")
def remove_group(self):
if len(self.selected_groups) > 0:
last_group = self.selected_groups.pop(-1)
current_text = self.selected_groups_text.get("1.0", tk.END).strip()
updated_text = "\n".join(
line for line in current_text.split("\n") if line != last_group
)
self.selected_groups_text.delete("1.0", tk.END)
self.selected_groups_text.insert(tk.END, updated_text)
def export_to_excel(self):
"""Логика экспорта в Excel (ИСПРАВЛЕННАЯ ВЕРСИЯ)."""
selected_group_names = (
self.selected_groups_text.get("1.0", tk.END).strip().split("\n")
)
# Составляем список кодов групп на основе выбранных названий
groups_codes = []
all_db_groups = list_groups()
for name in selected_group_names:
if name:
group = next((g for g in all_db_groups if g.name == name), None)
if group:
groups_codes.append(group.code)
if not groups_codes:
messagebox.showwarning("Предупреждение", "Не выбрано ни одной группы.")
return
try:
num_experts = int(self.num_experts.get())
if num_experts <= 0:
raise ValueError
except Exception:
messagebox.showerror(
"Ошибка", "Количество экспертов должно быть положительным числом."
)
return
experts_folder = os.path.join(os.path.dirname(__file__), "..", "experts")
os.makedirs(experts_folder, exist_ok=True)
for expert_num in range(1, num_experts + 1):
output_filename = os.path.join(experts_folder, f"Эксперт_{expert_num}.xlsx")
create_excel_template(groups_codes, output_filename)
messagebox.showinfo(
title="Экспорт завершён",
message=f"Успешно создано {num_experts} файлов экспертов.\n\nРасположение файлов:\n{experts_folder}",
)
# Файл-донор, создавался для обработки Эксель файлов
# в новом файле отправки в базу не будет.
# После того как будет сделана нормализация - ее результаты должны вернуться в
# соответствующую группу в крайний правый столбец и быть занесены напротив своих показателей.
import os
import re
import tkinter as tk
import numpy as np
from pathlib import Path
from statistics import mode as stat_mode
from tkinter import filedialog, messagebox, ttk
from modules.database_module import Group, Indicator, MatrixEntry, Session, calculate_indicator_weights
import pandas as pd
from openpyxl import Workbook, load_workbook
from openpyxl.styles import PatternFill
# --- Импорт из модуля базы данных ---
from modules.database_module import Group, Indicator, MatrixEntry, Session
# --- Вспомогательные функции ---
def normalize_value_to_strict(value):
if value is None or str(value).strip() == "":
return "1"
val_str = str(value).replace(",", ".").strip()
try:
num = float(val_str)
if abs(num - 1.0) < 0.1:
return "1"
elif abs(num - 3.0) < 0.1:
return "3"
elif num < 1.0:
return "0.333"
else:
return "3"
except ValueError:
return "1"
def is_transitive_matrix_upper_triangular(matrix):
violations = 0
valid_comparisons = 0
n = len(matrix)
for i in range(n):
for j in range(i + 1, n):
aij = matrix[i][j]
if abs(aij) < 1e-6:
continue
valid_comparisons += 1
for k in range(j + 1, n):
ajk = matrix[j][k]
aik = matrix[i][k]
if abs(ajk) < 1e-6 or abs(aik) < 1e-6:
continue
if (aij >= ajk and aik < ajk) or (aij <= ajk and aik > ajk):
violations += 1
if valid_comparisons == 0:
return 0.0
return max(0.0, ((valid_comparisons - violations) / valid_comparisons) * 100)
def fix_transitivity_in_matrix(matrix):
n = len(matrix)
fixed_matrix = [row.copy() for row in matrix]
for i in range(n):
for j in range(i + 1, n):
for k in range(j + 1, n):
aij = fixed_matrix[i][j]
ajk = fixed_matrix[j][k]
aik = fixed_matrix[i][k]
if (aij >= ajk and aik < ajk) or (aij <= ajk and aik > ajk):
if aij >= ajk:
fixed_matrix[i][k] = ajk
else:
fixed_matrix[i][k] = 1 / ajk if ajk != 0 else 1.0
return fixed_matrix
def aggregate_matrices_by_mode(matrices):
result = []
max_rows = max(len(mat) for mat in matrices)
max_cols = max(len(row) for mat in matrices for row in mat)
for row_idx in range(max_rows):
row = []
for col_idx in range(max_cols):
cell_values = []
for matrix in matrices:
if row_idx < len(matrix) and col_idx < len(matrix[row_idx]):
cell_values.append(matrix[row_idx][col_idx])
if cell_values:
try:
most_common_val = stat_mode(cell_values)
row.append(most_common_val)
except Exception:
row.append(1.0)
else:
row.append(None)
result.append(row)
return result
def create_empty_excel_template(filename):
workbook = Workbook()
workbook.save(filename)
# --- Класс окна ---
class AggregateFilesWindow(tk.Toplevel):
def __init__(self, parent):
super().__init__(parent)
self.title("Агрегирование файлов")
self.geometry("470x250")
main_frame = ttk.Frame(self)
main_frame.pack(padx=10, pady=5, fill="both", expand=True)
main_frame.columnconfigure(0, weight=1)
main_frame.columnconfigure(1, weight=2)
main_frame.columnconfigure(2, weight=1)
tk.Label(main_frame, text="Название создаваемого файла:").grid(
row=0, column=0, sticky="w"
)
self.new_file_name_entry = tk.Entry(main_frame, width=20)
self.new_file_name_entry.grid(row=0, column=1, sticky="we", pady=5)
# ИЗМЕНЕНИЕ: Кнопка теперь вызывает метод с валидацией
create_file_button = ttk.Button(
main_frame, text="Создать файл", command=self.create_file_with_validation
)
create_file_button.grid(row=0, column=2, sticky="e", pady=5)
aggregate_button = ttk.Button(
main_frame, text="Объединить информацию", command=self.aggregate_data
)
aggregate_button.grid(row=1, column=0, columnspan=4, sticky="we", pady=5)
tk.Label(main_frame, text="Выберите файл Excel:").grid(
row=2, column=0, sticky="w"
)
self.file_path_entry = tk.Entry(main_frame, width=30)
self.file_path_entry.grid(row=2, column=1, sticky="we", pady=5)
browse_button = ttk.Button(
main_frame, text="Обзор...", command=self.select_file_for_aggregation
)
browse_button.grid(row=2, column=2, sticky="e", pady=5)
tk.Label(main_frame, text="Выберите лист (группу):").grid(
row=3, column=0, sticky="w"
)
self.sheet_combobox = ttk.Combobox(main_frame, state="readonly", width=20)
self.sheet_combobox.grid(row=3, column=1, sticky="we", pady=5)
self.sheet_combobox.bind(
"<<ComboboxSelected>>", lambda _: self.reset_transitivity_field()
)
tk.Label(main_frame, text="Проверить на транзитивность:").grid(
row=4, column=0, sticky="w"
)
self.transitivity_percentage_label = tk.Label(
main_frame, text="", relief="groove", width=8
)
self.transitivity_percentage_label.grid(row=4, column=1, sticky="w", pady=5)
check_button = ttk.Button(
main_frame, text="Проверить", command=self.check_transitivity
)
check_button.grid(row=4, column=2, sticky="e", pady=5)
bottom_buttons_frame = ttk.Frame(main_frame)
bottom_buttons_frame.grid(row=5, column=0, columnspan=4, sticky="we", pady=5)
fix_transitivity_button = ttk.Button(
bottom_buttons_frame,
text="Исправить транзитивность",
command=self.apply_transitivity_corrections,
)
fix_transitivity_button.grid(row=0, column=0, sticky="we", padx=5)
send_to_db_button = ttk.Button(
bottom_buttons_frame,
text="Отправить в базу данных",
command=self.send_to_database,
)
send_to_db_button.grid(row=0, column=1, sticky="we", padx=5)
# ИЗМЕНЕНИЕ: Метод с валидацией имени файла
def create_file_with_validation(self):
filename = self.new_file_name_entry.get().strip()
# Проверка на пустое имя
if not filename:
messagebox.showwarning("Предупреждение", "Имя файла не может быть пустым.")
return
# Проверка на запрещенные символы в имени файла Windows и Excel
invalid_chars_pattern = r'[\\/:*?"<>|]'
if re.search(invalid_chars_pattern, filename):
messagebox.showerror(
"Ошибка", f'Имя файла содержит недопустимые символы: \\ / : * ? " < > |'
)
return
# Проверка на зарезервированные имена Windows (CON , PRN , AUX , NUL и
# т.д.)
reserved_names = [
"CON",
"PRN",
"AUX",
"NUL",
"COM1",
"COM2",
"COM3",
"COM4",
"COM5",
"COM6",
"COM7",
"COM8",
"COM9",
"LPT1",
"LPT2",
"LPT3",
"LPT4",
"LPT5",
"LPT6",
"LPT7",
"LPT8",
"LPT9",
]
# Получаем имя без расширения и приводим к верхнему регистру для
# сравнения
base_name = Path(filename).stem.upper()
if base_name in reserved_names:
messagebox.showerror(
"Ошибка",
f"'{base_name}' — зарезервированное имя. Пожалуйста выберите другое.",
)
return
# Если имя прошло все проверки — создаем файл.
try:
project_root = Path(__file__).resolve().parents[1]
experts_folder = project_root / "experts"
os.makedirs(experts_folder, exist_ok=True)
full_path = experts_folder / filename
# Проверяем наличие расширения .xlsx или .xlsm и добавляем .xlsx
# если его нет.
if not filename.endswith((".xlsx", ".xlsm")):
full_path = full_path.with_name(full_path.name + ".xlsx")
if not full_path.exists():
create_empty_excel_template(full_path)
messagebox.showinfo("Готово", f"Файл '{full_path.name}' создан.")
else:
messagebox.showinfo(
"Готово", f"Файл '{full_path.name}' уже существует."
)
except Exception as e:
messagebox.showerror("Ошибка", f"Не удалось создать файл: {e}")
def select_file_for_aggregation(self):
file_path = filedialog.askopenfilename(
filetypes=[
("Excel Files", "*.xlsx"),
("Excel Macro Enabled Workbooks", "*.xlsm"),
]
)
if file_path:
self.file_path_entry.delete(0, tk.END)
self.file_path_entry.insert(0, file_path)
self.load_sheets_from_file(file_path)
messagebox.showinfo("Выбор файла", f"Файл выбран: {file_path}")
def load_sheets_from_file(self, file_path):
try:
workbook = load_workbook(file_path)
sheets = workbook.sheetnames
self.sheet_combobox["values"] = sheets
if sheets:
self.sheet_combobox.current(0)
except Exception as e:
messagebox.showerror("Ошибка", f"Не удалось загрузить листы: {e}")
def reset_transitivity_field(self):
self.transitivity_percentage_label.config(text="")
def check_transitivity(self):
file_path = self.file_path_entry.get()
selected_sheet = self.sheet_combobox.get()
if not file_path or not selected_sheet:
messagebox.showwarning("Внимание", "Пожалуйста, укажите файл и лист!")
return
try:
wb = load_workbook(file_path)
ws = wb[selected_sheet]
matrix_numbers = []
for row in ws.iter_rows(min_row=2):
current_row = []
for cell in row:
val = cell.value
if val is None or val == "" or str(val).lower() == "none":
current_row.append(1.0)
else:
try:
num = float(str(val).replace(",", "."))
current_row.append(num)
except BaseException:
current_row.append(1.0)
matrix_numbers.append(current_row)
matrix_for_check = [row[3:] for row in matrix_numbers]
percent = is_transitive_matrix_upper_triangular(matrix_for_check)
self.transitivity_percentage_label.config(text=f"{percent:.1f}%")
except Exception as e:
print(f"Ошибка при проверке транзитивности: {e}")
self.transitivity_percentage_label.config(text="Ошибка")
def apply_transitivity_corrections(self):
# --- ОСТАВЛЯЕМ КОД БЕЗ ИЗМЕНЕНИЙ ---
file_path = self.file_path_entry.get()
selected_sheet = self.sheet_combobox.get()
if not file_path or not selected_sheet:
messagebox.showwarning("Внимание", "Пожалуйста, укажите файл и лист!")
return
try:
wb = load_workbook(file_path)
ws = wb[selected_sheet]
matrix_numbers = []
for row in ws.iter_rows(min_row=2):
current_row = []
for cell in row:
val = cell.value
if val is None or val == "" or str(val).lower() == "none":
current_row.append(1.0)
else:
try:
num = float(str(val).replace(",", "."))
current_row.append(num)
except BaseException:
current_row.append(1.0)
matrix_numbers.append(current_row)
matrix_for_fix = [row[3:] for row in matrix_numbers]
upper_fixed_matrix = fix_transitivity_in_matrix(matrix_for_fix.copy())
n = len(upper_fixed_matrix)
final_matrix_numbers = [[0.0 for _ in range(n)] for _ in range(n)]
for i in range(n):
for j in range(n):
if i == j:
final_matrix_numbers[i][j] = 1.0
elif i < j:
final_matrix_numbers[i][j] = upper_fixed_matrix[i][j]
else:
val = upper_fixed_matrix[j][i]
final_matrix_numbers[i][j] = 1.0 / val if val != 0 else 1.0
data_to_write = []
for row in final_matrix_numbers:
new_row = []
for num in row:
if abs(num - 1) < 0.1:
new_row.append("1")
elif num < 1.0:
new_row.append("0.333")
else:
new_row.append("3")
data_to_write.append(new_row)
start_row = 2
start_col = 4
for i, row in enumerate(data_to_write):
for j, value in enumerate(row):
ws.cell(row=start_row + i, column=start_col + j, value=value)
cell = ws.cell(row=start_row + i, column=start_col + j)
cell.number_format = "@"
if i == j:
cell.fill = PatternFill(
start_color="FFFF00", end_color="FFFF00", fill_type="solid"
)
wb.save(file_path)
messagebox.showinfo("Готово", "Транзитивность исправлена и файл сохранён.")
except Exception as e:
messagebox.showerror("Ошибка", f"Не удалось исправить: {e}")
def aggregate_data(self):
# --- ОСТАВЛЯЕМ КОД БЕЗ ИЗМЕНЕНИЙ ---
output_file = self.new_file_name_entry.get()
if not output_file:
messagebox.showwarning("Предупреждение", "Сначала создайте имя файла.")
return
if not output_file.endswith(".xlsx"):
output_file += ".xlsx"
project_root = Path(__file__).resolve().parents[1]
experts_folder = project_root / "experts"
os.makedirs(experts_folder, exist_ok=True)
full_output_path = experts_folder / output_file
file_paths = filedialog.askopenfilenames(
title="Выберите файлы экспертов", filetypes=[("Excel Files", "*.xlsx")]
)
if not file_paths:
messagebox.showwarning("Предупреждение", "Файлы не выбраны.")
return
sheets_results = {}
for path in file_paths:
try:
wb = load_workbook(path)
for sheet_name in wb.sheetnames:
ws = wb[sheet_name]
data = [[cell.value for cell in row] for row in ws.rows]
sheets_results.setdefault(sheet_name, []).append(data)
except Exception as e:
messagebox.showerror("Ошибка чтения", f"Ошибка в файле {path}: {e}")
return # Останавливаем процесс
if not sheets_results:
raise ValueError("Нет данных для объединения.")
new_wb = Workbook()
new_wb.remove(new_wb.active) # Удаляем лист по умолчанию
for sheet_name, matrices_list in sheets_results.items():
agg_matrix = aggregate_matrices_by_mode(matrices_list)
new_ws = new_wb.create_sheet(title=sheet_name)
for r_idx, row in enumerate(agg_matrix):
for c_idx, val in enumerate(row):
new_ws.cell(row=r_idx + 1, column=c_idx + 1, value=str(val))
new_wb.save(full_output_path)
new_wb.close()
messagebox.showinfo("Готово", f"Данные объединены в {output_file}.")
# --- ОТПРАВКА В БАЗУ ДАННЫХ ---
def send_to_database(self):
file_path = self.file_path_entry.get()
selected_sheet = self.sheet_combobox.get()
if not file_path:
messagebox.showwarning("Внимание", "Пожалуйста, выберите файл Excel!")
return
if not selected_sheet:
messagebox.showwarning("Внимание", "Пожалуйста, выберите лист (группу)!")
return
try:
# Читаем данные из выбранного листа Excel
df = pd.read_excel(file_path, sheet_name=selected_sheet, header=None)
session = Session()
# Ищем группу по названию листа
group = session.query(Group).filter_by(name=selected_sheet).first()
if not group:
# Если группы нет, создаем новую
group_code = "".join(filter(str.isalnum, selected_sheet.lower()))
group = Group(name=selected_sheet, code=group_code)
session.add(group)
# --- ВАЖНО: Сбрасываем сессию, чтобы у ГРУППЫ появился ID ---
session.flush()
# Очищаем старые данные этой группы перед импортом новых
session.query(MatrixEntry).filter_by(group_id=group.id).delete()
session.query(Indicator).filter_by(group_code=group.code).delete()
# --- ЧТЕНИЕ ИНДИКАТОРОВ (Названий) ---
indicators_to_add = []
for row_idx, row in df.iloc[1:].iterrows():
indicator_name = str(row[1]).strip()
# Если ячейка пустая, пропускаем строку
if not indicator_name:
continue
indicator_code = f"{group.code}-{row_idx}"
# Создаем объект индикатора и добавляем в список
indicators_to_add.append(
Indicator(
name=indicator_name,
code=indicator_code,
group_code=group.code, # Привязываем к группе по коду
)
)
# Сохраняем все индикаторы в базу одним запросом
if indicators_to_add:
session.bulk_save_objects(indicators_to_add)
# --- ЧТЕНИЕ МАТРИЦЫ (Значений) ---
matrix_entries = []
for r_idx, row in df.iloc[1:].iterrows():
for c_offset, cell_value in enumerate(row[3:], start=0):
try:
if pd.isna(cell_value):
continue
num = float(str(cell_value).replace(",", "."))
# Нормализация значений к 1 или 3
if abs(num - 1.0) < 0.1:
norm_value = 1.0
elif abs(num - 3.0) < 0.1:
norm_value = 3.0
elif num < 1.0:
norm_value = 1 / 3
else:
norm_value = 3.0
matrix_entries.append(
MatrixEntry(
group_id=group.id,
row=r_idx - 1,
col=c_offset,
value=norm_value,
)
)
except (ValueError, TypeError):
continue
# Сохраняем все записи матрицы одним запросом
if matrix_entries:
session.bulk_save_objects(matrix_entries)
# Финальное подтверждение всех изменений в базе данных
session.commit()
except Exception as e:
# Если что-то пошло не так, откатываем изменения (это важно!)
session.rollback()
messagebox.showerror("Ошибка", f"Не удалось отправить данные: {e}")
finally:
# Обязательно закрываем сессию в конце
session.close()
messagebox.showinfo("Готово", "Данные успешно отправлены в базу данных.")
def send_to_database(self):
file_path = self.file_path_entry.get()
# Проверка выбора файла
if not file_path:
messagebox.showwarning("Внимание", "Пожалуйста, выберите файл Excel!")
return
try:
# Получаем список всех листов в файле
all_sheets = pd.ExcelFile(file_path).sheet_names
if not all_sheets:
messagebox.showwarning("Внимание", "В файле нет листов для обработки!")
return
session = Session()
success = True # Флаг для отслеживания общего успеха
# Функция для обработки ОДНОГО листа (группы)
def process_sheet(sheet_name, session_obj):
print(f"\n--- Начало обработки листа: {sheet_name} ---")
df = pd.read_excel(file_path, sheet_name=sheet_name, header=None)
# Поиск или создание группы
group = session_obj.query(Group).filter_by(name=sheet_name).first()
if not group:
group_code = "".join(filter(str.isalnum, sheet_name.lower()))
group = Group(name=sheet_name, code=group_code)
session_obj.add(group)
session_obj.flush() # Чтобы у группы появился ID
# Очистка старых данных группы
session_obj.query(MatrixEntry).filter_by(group_id=group.id).delete()
session_obj.query(Indicator).filter_by(group_code=group.code).delete()
# --- ЧТЕНИЕ ИНДИКАТОРОВ (Названий) ---
indicators_to_add = []
for row_idx, row in df.iloc[1:].iterrows():
indicator_name = str(row[1]).strip()
if not indicator_name:
continue
indicator_code = f"{group.code}-{row_idx}"
indicators_to_add.append(
Indicator(
name=indicator_name,
code=indicator_code,
group_code=group.code,
)
)
# Сохраняем индикаторы в БД
if indicators_to_add:
session_obj.bulk_save_objects(indicators_to_add)
session_obj.flush() # Сохраняем, чтобы получить ID индикаторов
# --- ЧТЕНИЕ МАТРИЦЫ (Значений) ---
matrix_entries = []
for r_idx, row in df.iloc[1:].iterrows():
for c_offset, cell_value in enumerate(row[3:], start=0):
try:
if pd.isna(cell_value):
continue
num = float(str(cell_value).replace(",", "."))
# Нормализация значений
if abs(num - 1.0) < 0.1:
norm_value = 1.0
elif abs(num - 3.0) < 0.1:
norm_value = 3.0
elif num < 1.0:
norm_value = 1 / 3
else:
norm_value = 3.0
matrix_entries.append(
MatrixEntry(
group_id=group.id,
row=r_idx - 1,
col=c_offset,
value=norm_value,
)
)
except (ValueError, TypeError):
continue
# Сохраняем матрицу в БД
if matrix_entries:
session_obj.bulk_save_objects(matrix_entries)
# --- РАСЧЕТ ВЕСОВ (КОВ) ---
# Получаем только что сохраненные записи матрицы для этой группы
matrix_entries_for_calc = session_obj.query(MatrixEntry).filter_by(group_id=group.id).all()
indicator_weights = calculate_indicator_weights(matrix_entries_for_calc)
# --- СОХРАНЕНИЕ ВЕСОВ В БД ---
if indicator_weights is not None and len(indicator_weights) > 0:
# Получаем индикаторы этой группы в порядке их создания (по ID)
indicators_in_db = (
session_obj.query(Indicator)
.filter_by(group_code=group.code)
.order_by(Indicator.id.asc())
.all()
)
# Проверка на совпадение количества (защита от ошибок)
if len(indicators_in_db) == len(indicator_weights):
for idx, indicator in enumerate(indicators_in_db):
indicator.kov = indicator_weights[idx]
print(f"--- ВЕСА УСПЕШНО СОХРАНЕНЫ ДЛЯ ГРУППЫ: {sheet_name} ---")
success_local = True
else:
print(f"--- ОШИБКА: Несовпадение количества! Индикаторов: {len(indicators_in_db)}, Весов: {len(indicator_weights)} ---")
success_local = False
else:
print(f"--- ОШИБКА: Расчёт весов для группы {sheet_name} не удался. ---")
success_local = False
return success_local
# --- ГЛАВНЫЙ ЦИКЛ ПО ВСЕМ ЛИСТАМ ---
for sheet in all_sheets:
try:
sheet_success = process_sheet(sheet, session)
success = success and sheet_success # Если хоть одна группа не обработалась - общий успех False
except Exception as e:
print(f"--- Критическая ошибка при обработке листа '{sheet}': {e} ---")
success = False
# Финальное сохранение всех изменений (одной транзакцией)
if success:
session.commit()
except Exception as e:
session.rollback()
messagebox.showerror("Ошибка", f"Не удалось отправить данные: {e}")
finally:
session.close()
# Сообщение пользователю после завершения цикла
if success:
messagebox.showinfo("Готово", "Данные успешно отправлены в базу данных. Расчёт весов выполнен.")
else:
messagebox.showerror("Ошибка", "Обработка завершена с ошибками. Проверьте лог в консоли.")
# Dozor\modules\excel\excel_aggregate_inter.py
# Файл-донор
# -*- coding: utf-8 -*-
"""
Интерфейс окна 'Агрегирование файлов' для Эксперта.
"""
import re
from tkinter import filedialog, messagebox, ttk
import tkinter as tk
from modules.excel.excel_aggregate_work import AggregateWorkController
from modules.utils.window_behavior import LevelTwoBehavior
class ExpertAggregateInterface(tk.Toplevel):
def __init__(self, parent):
super().__init__(parent)
# --- НОВАЯ СТРОКА: Применяем поведение Уровня 2 ---
behavior = LevelTwoBehavior()
behavior.setup_level_two(self)
self.title("Агрегирование файлов")
self.geometry("470x250")
self.resizable(False, False)
self.controller = AggregateWorkController()
main_frame = ttk.Frame(self)
main_frame.pack(padx=10, pady=5, fill="both", expand=True)
main_frame.columnconfigure(1, weight=1)
# --- Создание нового файла ---
ttk.Label(main_frame, text="Название создаваемого файла:").grid(
row=0, column=0, sticky="w"
)
self.new_file_name_entry = tk.Entry(main_frame, width=20)
self.new_file_name_entry.grid(row=0, column=1, sticky="we", pady=5)
create_file_button = ttk.Button(
main_frame, text="Создать файл", command=self.create_file_with_validation
)
create_file_button.grid(row=0, column=2, sticky="e", pady=5)
# --- Объединение информации ---
aggregate_button = ttk.Button(
main_frame, text="Объединить информацию", command=self.aggregate_data
)
aggregate_button.grid(row=1, column=0, columnspan=3, sticky="we", pady=5)
# --- Выбор существующего файла ---
ttk.Label(main_frame, text="Выберите файл Excel:").grid(
row=2, column=0, sticky="w"
)
self.file_path_entry = tk.Entry(main_frame, width=30)
self.file_path_entry.grid(row=2, column=1, sticky="we", pady=5)
browse_button = ttk.Button(
main_frame, text="Обзор...", command=self.select_file_for_aggregation
)
browse_button.grid(row=2, column=2, sticky="e", pady=5)
# --- Выбор листа ---
ttk.Label(main_frame, text="Выберите лист (группу):").grid(
row=3, column=0, sticky="w"
)
self.sheet_combobox = ttk.Combobox(main_frame, state="readonly", width=20)
self.sheet_combobox.grid(row=3, column=1, sticky="we", pady=5)
self.sheet_combobox.bind(
"<<ComboboxSelected>>", lambda _: self.reset_transitivity_field()
)
# --- Проверка транзитивности ---
ttk.Label(main_frame, text="Проверить на транзитивность:").grid(
row=4, column=0, sticky="w"
)
self.transitivity_percentage_label = tk.Label(
main_frame, text="", relief="groove", width=8
)
self.transitivity_percentage_label.grid(row=4, column=1, sticky="w", pady=5)
check_button = ttk.Button(
main_frame, text="Проверить", command=self.check_transitivity
)
check_button.grid(row=4, column=2, sticky="e", pady=5)
# --- Нижние кнопки действий ---
bottom_buttons_frame = ttk.Frame(main_frame)
bottom_buttons_frame.grid(row=5, column=0, columnspan=3, sticky="we", pady=5)
fix_transitivity_button = ttk.Button(
bottom_buttons_frame,
text="Исправить транзитивность",
command=self.apply_transitivity_corrections,
)
fix_transitivity_button.grid(row=0, column=0, sticky="we", padx=5)
send_to_db_button = ttk.Button(
bottom_buttons_frame,
text="Отправить в базу данных",
command=self.send_to_database,
)
send_to_db_button.grid(row=0, column=1, sticky="we", padx=5)
# === МЕТОДЫ ИНТЕРФЕЙСА (связь с контроллером) ===
def create_file_with_validation(self):
filename = self.new_file_name_entry.get().strip()
invalid_chars_pattern = r'[\\/:*?"<>|]'
if not filename:
messagebox.showwarning("Предупреждение", "Имя файла не может быть пустым.")
return
if re.search(invalid_chars_pattern, filename):
messagebox.showerror("Ошибка", "Имя файла содержит недопустимые символы.")
return
reserved_names = [
"CON", "PRN", "AUX", "NUL", "COM1", "LPT1", "COM2", "LPT2",
"COM3", "LPT3", "COM4", "LPT4", "COM5", "LPT5"
]
base_name = filename.split(".")[0].upper()
if base_name in reserved_names:
messagebox.showerror("Ошибка", "'{}' — зарезервированное имя.".format(base_name))
return
self.controller.create_empty_excel_template(filename)
messagebox.showinfo("Готово", f"Файл '{filename}' создан.")
def select_file_for_aggregation(self):
file_path = filedialog.askopenfilename(
filetypes=[("Excel Files", "*.xlsx"), ("Excel Macro Enabled Workbooks", "*.xlsm")]
)
if file_path:
self.file_path_entry.delete(0, tk.END)
self.file_path_entry.insert(0, file_path)
sheets = self.controller.load_sheets_from_file(file_path)
self.sheet_combobox["values"] = sheets
if sheets:
self.sheet_combobox.current(0)
messagebox.showinfo("Выбор файла", f"Файл выбран: {file_path}")
def reset_transitivity_field(self):
self.transitivity_percentage_label.config(text="")
def check_transitivity(self):
file_path = self.file_path_entry.get()
selected_sheet = self.sheet_combobox.get()
if not file_path or not selected_sheet:
messagebox.showwarning("Внимание", "Пожалуйста, укажите файл и лист!")
return
result = self.controller.check_transitivity()
self.transitivity_percentage_label.config(text=result)
def apply_transitivity_corrections(self):
file_path = self.file_path_entry.get()
selected_sheet = self.sheet_combobox.get()
if not file_path or not selected_sheet:
messagebox.showwarning("Внимание", "Пожалуйста, укажите файл и лист!")
return
self.controller.apply_transitivity_corrections()
messagebox.showinfo("Готово", "Транзитивность исправлена (заглушка).")
def aggregate_data(self):
output_file = self.new_file_name_entry.get()
if not output_file:
messagebox.showwarning("Предупреждение", "Сначала создайте имя файла.")
return
if not output_file.endswith(".xlsx"):
output_file += ".xlsx"
file_paths = filedialog.askopenfilenames(
title="Выберите файлы экспертов", filetypes=[("Excel Files", "*.xlsx")]
)
if not file_paths:
messagebox.showwarning("Предупреждение", "Файлы не выбраны.")
return
success = self.controller.aggregate_data(output_file, file_paths)
if success:
messagebox.showinfo("Готово", f"Данные объединены в {output_file}.")
def send_to_database(self):
file_path = self.file_path_entry.get()
if not file_path:
messagebox.showwarning("Внимание", "Пожалуйста, выберите файл Excel!")
return
self.controller.send_to_database(file_path)
messagebox.showinfo("Готово", "Данные успешно отправлены в базу данных (заглушка).")
# Dozor/modules/excel/excel_aggregate_work.py
# Файл-донор
import os
from pathlib import Path
class AggregateWorkController:
def __init__(self):
self.file_path = ""
self.selected_sheet = ""
self._fake_sheets_cache = ["Группа А", "Группа Б"] # Фейковые данные листа
def select_file(self):
"""Возвращает фейковый путь, если пользователь не выбрал файл сам."""
project_root = Path(__file__).resolve().parents[2]
experts_folder = project_root / "experts"
os.makedirs(experts_folder, exist_ok=True)
fake_file = experts_folder / "Эксперт_заглушка.xlsx"
if not fake_file.exists():
with open(fake_file, 'w') as f:
pass
return str(fake_file)
def load_sheets_from_file(self, file_path):
"""Если файл выбран - возвращаем фейковые листы, иначе пустой список."""
self.file_path = file_path
if file_path and os.path.exists(file_path):
return self._fake_sheets_cache
return []
def check_transitivity(self):
"""Заглушка проверки транзитности всегда возвращает успех."""
dummy_matrix = [[1.0, 3.0], [0.333, 1.0]]
percent = 100.0 # Всегда успешно для интерфейса
return f"{percent:.1f}%"
def apply_transitivity_corrections(self):
"""Пустая заглушка применения исправлений."""
pass
def create_empty_excel_template(self, filename):
"""Создает абсолютно пустой файл на диске."""
try:
project_root = Path(__file__).resolve().parents[2]
experts_folder = project_root / "experts"
os.makedirs(experts_folder, exist_ok=True)
full_path = experts_folder / filename
if not full_path.exists():
with open(full_path, 'w') as f:
pass
except Exception:
pass
def aggregate_data(self, output_file, input_files):
"""Просто создает целевой файл, имитируя объединение."""
if not input_files:
return False
project_root = Path(__file__).resolve().parents[2]
experts_folder = project_root / "experts"
os.makedirs(experts_folder, exist_ok=True)
dest_path = experts_folder / output_file
with open(dest_path, 'w') as f:
pass
return True
def send_to_database(self, file_path):
"""Заглушка отправки в базу данных."""
pass
# Dozor\modules\excel\excel_export_inter.py
# Файл-донор
# -*- coding: utf-8 -*-
"""
Интерфейс окна 'Создание файлов' для Эксперта.
"""
import tkinter as tk
from tkinter import ttk, messagebox
from modules.excel.excel_export_work import ExportWorkController
from modules.utils.window_behavior import LevelTwoBehavior
class ExpertExportInterface(tk.Toplevel):
def __init__(self, parent):
super().__init__(parent)
# --- НОВАЯ СТРОКА: Применяем поведение Уровня 2 ---
behavior = LevelTwoBehavior()
behavior.setup_level_two(self)
self.title("Создание файла для эксперта")
self.geometry("450x200")
self.resizable(False, False)
# Связываем интерфейс с контроллером (бизнес-логикой)
self.controller = ExportWorkController()
main_frame = ttk.Frame(self)
main_frame.pack(padx=10, pady=10, fill="both", expand=True)
# --- Группа ---
ttk.Label(main_frame, text="Группа:").grid(row=0, column=0, sticky="w")
self.group_var = tk.StringVar()
self.combobox = ttk.Combobox(
main_frame, textvariable=self.group_var, state="readonly", width=25
)
self.combobox.grid(row=0, column=1, padx=(5, 10), sticky="ew")
self.refresh_groups_list()
add_group_btn = ttk.Button(
main_frame, text="Добавить группу", command=self.add_group
)
add_group_btn.grid(row=0, column=2, padx=(0, 10), sticky="e")
# --- Выбранные группы ---
ttk.Label(main_frame, text="Выбранные группы:").grid(row=1, column=0, sticky="w")
self.selected_groups_text = tk.Text(main_frame, width=30, height=4)
self.selected_groups_text.grid(
row=1, column=1, columnspan=2, pady=(5, 10), sticky="ew"
)
# --- Количество экспертов ---
ttk.Label(main_frame, text="Количество экспертов:").grid(
row=2, column=0, sticky="w"
)
self.num_experts = tk.IntVar(value=1)
self.experts_count_entry = ttk.Entry(main_frame, textvariable=self.num_experts, width=5)
self.experts_count_entry.grid(row=2, column=1, sticky="w")
remove_group_btn = ttk.Button(
main_frame, text="Удалить группу", command=self.remove_group
)
remove_group_btn.grid(row=2, column=2, sticky="e")
# --- Кнопка экспорта ---
export_btn = ttk.Button(
main_frame, text="Экспортировать в Excel", command=self.export_to_excel
)
export_btn.grid(row=3, column=1, pady=(10, 0))
def refresh_groups_list(self):
"""Обновляет выпадающий список групп данными из контроллера."""
self.controller.refresh_groups()
self.combobox["values"] = self.controller.groups
if self.controller.groups:
self.combobox.current(0)
def add_group(self):
"""Пробрасывает команду добавления в контроллер и обновляет текстовое поле."""
group_name = self.group_var.get()
self.controller.add_group(group_name)
self._update_selected_text()
def remove_group(self):
"""Пробрасывает команду удаления в контроллер и обновляет текстовое поле."""
self.controller.remove_group()
self._update_selected_text()
def _update_selected_text(self):
"""Внутренний метод обновления виджета Text."""
current_text = "\n".join(self.controller.selected_groups)
self.selected_groups_text.delete("1.0", tk.END)
self.selected_groups_text.insert(tk.END, current_text)
def export_to_excel(self):
"""Собирает данные из полей и отдает их в рабочий модуль."""
selected_group_names = (
self.selected_groups_text.get("1.0", tk.END).strip().split("\n")
)
try:
num_experts = int(self.num_experts.get())
if num_experts <= 0:
raise ValueError
except Exception:
messagebox.showerror(
"Ошибка", "Количество экспертов должно быть положительным числом."
)
return
# Передаем только имена, поиск кодов теперь внутри work-модуля отсутствует
self.controller.export_to_excel(selected_group_names, num_experts)
messagebox.showinfo(
title="Экспорт завершён",
message=f"Успешно создано {num_experts} файлов.\n\nРасположение:\n.../experts/",
)
# Dozor/modules/excel/excel_export_work.py
# -*- coding: utf-8 -*-
"""
Модуль бизнес-логики для создания файлов экспертов.
ВРЕМЕННАЯ ЗАГУБКА: отключена работа с матрицей индикаторов.
"""
import os
from pathlib import Path
from openpyxl import Workbook
# Относительный импорт пакета database_module
try:
from ..database_module import list_groups
except ImportError:
# Если структура пакетов нарушена, создаем фейковую функцию
def list_groups():
class FakeGrp:
def __init__(self, name, code):
self.name = name
self.code = code
return [FakeGrp("Группа А", "GRP-01"), FakeGrp("Группа Б", "GRP-02")]
def create_excel_template(groups_codes, output_filename):
"""
Создает ПУСТОЙ шаблон Excel-файла.
Логика наполнения данными перенесена на потом.
"""
try:
project_root = Path(__file__).resolve().parents[2]
experts_folder = project_root / "experts"
os.makedirs(experts_folder, exist_ok=True)
full_path = experts_folder / output_filename
wb = Workbook()
ws = wb.active
ws.title = "Шаблон"
# Просто пишем заголовки, чтобы файл открывался без ошибок структуры
ws['A1'] = '№ п/п'
ws['B1'] = 'Показатель'
ws['C1'] = 'Код'
# Удаляем лист по умолчанию, если он называется Sheet
if "Sheet" in wb.sheetnames and wb["Sheet"].max_row == 1 and wb["Sheet"].max_column == 1:
del wb["Sheet"]
wb.save(full_path)
except Exception as e:
print(f"[EXPORT WORK ERROR] Не удалось создать заглушку {output_filename}: {e}")
class ExportWorkController:
def __init__(self):
self.groups = []
self.selected_groups = []
def refresh_groups(self):
"""Получаем ТОЛЬКО список имен групп."""
try:
db_groups = list_groups()
self.groups = [grp.name for grp in db_groups]
except Exception as e:
print(f"[ERROR] Не удалось загрузить группы: {e}")
self.groups = ["Ошибка загрузки"]
def add_group(self, group_name):
if group_name and group_name.strip() and group_name not in self.selected_groups:
self.selected_groups.append(group_name)
def remove_group(self):
if len(self.selected_groups) > 0:
self.selected_groups.pop(-1)
def export_to_excel(self, selected_group_names, num_experts):
"""
Главная функция экспорта. Теперь она безопасна.
"""
groups_codes = [] # Мы больше не ищем коды здесь, чтобы избежать запроса индикаторов
project_root = Path(__file__).resolve().parents[2]
experts_folder = project_root / "experts"
os.makedirs(experts_folder, exist_ok=True)
for expert_num in range(1, int(num_experts) + 1):
filename = f"Эксперт_{expert_num}.xlsx"
full_path = experts_folder / filename
# Вызываем упрощенную функцию-заглушку
create_excel_template(groups_codes, str(full_path))
# Dozor\modules\administrator\admin_ui.py
# Файл для образца/примера
import tkinter as tk
from tkinter import messagebox
# Импортируем ТВОИ старые рабочие модули.
# Убедись, что пути правильные! Если файлы лежат в другой папке, поправь путь здесь.
try:
from modules.administrator.admin_user_inter import AdminUserInter
except ImportError:
print("[WARNING] admin_user_inter.py не найден по стандартному пути.")
class AdminUserInter(tk.Toplevel): pass # Заглушка для теста
try:
from modules.administrator.admin_db_inter import AdminDBInter
except ImportError:
print("[WARNING] admin_db_inter.py не найден по стандартному пути.")
class AdminDBInter(tk.Toplevel): pass # Заглушка для теста
class AdminPanel(tk.Frame):
"""
Главный фасад модуля "Администратор".
Он создается внутри Главного Окна (MainInterface) и занимает его полностью.
При нажатии кнопок он открывает СТАРЫЕ модальные окна.
"""
def __init__(self, master, current_user):
# Используем нормальный светло-серый цвет вместо лайма
super().__init__(master, bg="#F5F5F5")
self.master_root = master # Сохраняем ссылку на главное окно (Root)
self.current_user = current_user
print("[DEBUG] AdminPanel создан.")
# Настраиваем сетку самого фасада (внутри него будут хедер и тело)
self.grid_rowconfigure(0, weight=1) # Хедер (фиксированный размер)
self.grid_rowconfigure(1, weight=5) # Основное тело (растягивается)
self.grid_columnconfigure(0, weight=1)
self._create_header()
self._create_body()
def _create_header(self):
"""Верхняя полоса с заголовком и кнопкой назад."""
header = tk.Frame(self, bg="#34495e", height=50)
header.grid(row=0, column=0, sticky="ew")
title = tk.Label(header, text="Администрирование", bg="#34495e", fg="white", font=("Arial", 16))
title.pack(side="left", padx=20, pady=10)
back_btn = tk.Button(header, text="← Назад", command=self.destroy, bg="#7f8c8d", fg="white")
back_btn.pack(side="right", padx=20, pady=10)
def _create_body(self):
"""Центральная часть с двумя большими кнопками."""
body = tk.Frame(self, bg="white") # Белый фон рабочей области
body.grid(row=1, column=0, sticky="nsew", padx=30, pady=30)
# Делим тело на две равные колонки
body.grid_columnconfigure(0, weight=1)
body.grid_columnconfigure(1, weight=1)
users_btn = tk.Button(body, text="Пользователи", font=("Arial", 14), width=18, height=2,
command=self.open_users_module,
bg="#3498db", fg="white", relief="raised", bd=2)
users_btn.grid(row=0, column=0, padx=10, pady=10, sticky="nsew")
db_btn = tk.Button(body, text="Базы данных", font=("Arial", 14), width=18, height=2,
command=self.open_db_module,
bg="#27ae60", fg="white", relief="raised", bd=2)
db_btn.grid(row=0, column=1, padx=10, pady=10, sticky="nsew")
def open_users_module(self):
"""Открывает старое окно управления пользователями."""
print("[ACTION] Открытие Админ-Пользователей...")
# Блокируем интерфейс через утилиты приложения
from modules.utils.app_state import disable_app, enable_app
disable_app(self.master_root)
# Создаем экземпляр старого рабочего класса
users_win = AdminUserInter(self.master_root)
# Регистрируем его в системе окон Dozor (чтобы знать о дочерних окнах)
if hasattr(self.master_root, 'register_child_window'):
self.master_root.register_child_window(users_win, offset_x=20, offset_y=20)
# Ждем закрытия окна (модальность)
self.master_root.wait_window(users_win)
# Разблокируем интерфейс
enable_app(self.master_root)
print("[ACTION] Модуль Пользователей закрыт.")
def open_db_module(self):
"""Запускает рабочее окно управления базами данных."""
# Блокируем главное приложение Dozor
from modules.utils.app_state import disable_app, enable_app
disable_app(self.master_root)
try:
# Импортируем ТВОЙ РАБОЧИЙ ИНТЕРФЕЙС
# ЗАМЕНИ admin_db_inter НА ТОТ ФАЙЛ, КОТОРЫЙ У ТЕБЯ ЕСТЬ!
# Например, если он называется db_backup.py или backup_manager.py
from modules.administrator.admin_db_inter import AdminDBInter
# Создаем модальное окно
db_win = AdminDBInter(self.master_root) # Или self.parent_main, если у тебя так
# Регистрируем его в системе окон (если у вас есть такой механизм)
if hasattr(self.master_root, 'register_child_window'):
self.master_root.register_child_window(db_win, offset_x=20, offset_y=20)
# Ждем закрытия этого окна
self.master_root.wait_window(db_win)
except Exception as e:
print(f"[FATAL] Ошибка запуска модуля Баз Данных: {e}")
messagebox.showerror("Ошибка", f"Не удалось открыть модуль:\\n{e}")
finally:
enable_app(self.master_root)
# Dozor/modules/utils/window_behavior.py
# Файл с описанием поведения окон.
import tkinter as tk
class LevelOneBehavior:
"""
Поведение Главного окна (Уровень 1). Применяется к объекту класса tk.Tk.
Движение работает ТОЛЬКО по панели заголовка (title_bar).
"""
def setup_level_one(self, target_window):
target_window.resizable(False, False)
# Ищем панель заголовка для перетаскивания главного окна
try:
self.drag_widget = target_window.children['title_bar']
except KeyError:
self.drag_widget = target_window
self.drag_widget.bind("<ButtonPress-1>", self.save_offset)
self.drag_widget.bind("<B1-Motion>", self.drag_window)
target_window.bind("<Double-Button-1>", lambda e: None)
if not hasattr(target_window, '_child_windows'):
target_window._child_windows = []
self._drag_start_x = 0
self._drag_start_y = 0
def save_offset(self, event):
self._drag_start_x = event.x_root - self.winfo_x()
self._drag_start_y = event.y_root - self.winfo_y()
def drag_window(self, event):
x = event.x_root - self._drag_start_x
y = event.y_root - self._drag_start_y
self.geometry(f"+{x}+{y}")
if hasattr(self, '_update_child_positions'):
self._update_child_positions(x, y)
def register_child_window(self, window, offset_x=0, offset_y=0):
self._child_windows.append((window, offset_x, offset_y))
def do_nothing(self, event=None):
pass
class LevelTwoBehavior:
"""
Поведение окон второго уровня (привязаны к родителю).
Окно центрируется над главным и едет строго вместе с ним.
РЕЖИМ БЕЗОПАСНОСТИ: Игнорируем кастомные методы регистрации root-а.
"""
def setup_level_two(self, target_window):
width, height = int(target_window.winfo_reqwidth()), int(target_window.winfo_reqheight())
target_window.minsize(width, height)
target_window.resizable(True, True)
# Просто берем родителя окна (это наш ManagerPanel/Facade)
parent = target_window.master
p_x = parent.winfo_rootx()
p_y = parent.winfo_rooty()
p_w = parent.winfo_width()
p_h = parent.winfo_height()
win_w = target_window.winfo_reqwidth()
win_h = target_window.winfo_reqheight()
x = p_x + (p_w - win_w) // 2
y = p_y + (p_h - win_h) // 2
target_window.geometry(f"+{x}+{y}")
target_window.update_idletasks()
# === КРИТИЧЕСКОЕ ИЗМЕНЕНИЕ ===
# Весь блок с register_child_window УДАЛЕН.
# Если родитель хочет знать о нас, он сам вызовет wait_window().
def do_nothing(self, event=None):
pass
def save_offset(self, event, window):
self._start_screen_x = event.x_root
self._start_screen_y = event.y_root
self._start_win_x = window.winfo_rootx()
self._start_win_y = window.winfo_rooty()
parent = window.master
self._start_parent_x = parent.winfo_rootx()
self._start_parent_y = parent.winfo_rooty()
def drag_with_parent(self, event, child_win, parent_win):
delta_x = event.x_root - self._start_screen_x
delta_y = event.y_root - self._start_screen_y
new_parent_x = self._start_parent_x + delta_x
new_parent_y = self._start_parent_y + delta_y
parent_win.geometry(f"+{new_parent_x}+{new_parent_y}")
offset_x = self._start_win_x - self._start_parent_x
offset_y = self._start_win_y - self._start_parent_y
target_child_x = new_parent_x + offset_x
target_child_y = new_parent_y + offset_y
child_win.geometry(f"+{target_child_x}+{target_child_y}")
class LevelThreeBehavior:
"""
Поведение окон третьего уровня (Менеджер групп).
Свободное перемещение.
Чтобы выделить текст, просто кликай мышкой.
Чтобы двигать окно, зажми ALT + ЛКМ.
"""
def setup_level_three(self, target_window):
width, height = int(target_window.winfo_reqwidth()), int(target_window.winfo_reqheight())
target_window.minsize(width, height)
target_window.resizable(True, True)
target_window.geometry("+0+0")
target_window.protocol("WM_MINIMIZE_WINDOW", self.do_nothing)
target_window.protocol("WM_DELETE_WINDOW", target_window.destroy)
# Снимаем старые бинды
target_window.unbind("<ButtonPress-1>")
target_window.unbind("<B1-Motion>")
# 1. Сохраняем точку старта. Передаем 'target_window' вторым аргументом через lambda
target_window.bind(
"<ButtonPress-1>",
lambda e, tw=target_window: self.save_offset(e, tw),
add='+'
)
# 2. Движение работает ТОЛЬКО если зажата клавиша Alt (маска 0x0004)
target_window.bind(
"<B1-Motion>",
lambda e, tw=target_window: self.drag_free(e, tw) if e.state & 0x0004 else "",
add='+'
)
def do_nothing(self, event=None):
pass
def save_offset(self, event, window):
# Запоминаем ГДЕ НА ЭКРАНЕ была нажата мышь
self._start_screen_x = event.x_root
self._start_screen_y = event.y_root
# Запоминаем, где В ЭТОТ МОМЕНТ было окно (его угол) ПО ЭКРАНУ
self._start_win_x = window.winfo_rootx()
self._start_win_y = window.winfo_rooty()
def drag_free(self, event, window):
dx = event.x_root - self._start_screen_x
dy = event.y_root - self._start_screen_y
new_x = self._start_win_x + dx
new_y = self._start_win_y + dy
window.geometry(f"+{new_x}+{new_y}")
# Dozor\gui\main_interface.py
# Основной файл,главное окно.
import tkinter as tk
from tkinter import ttk
from PIL import Image, ImageTk
import os
from modules.administrator.admin_ui import AdminPanel
class MainInterface(tk.Tk):
def __init__(self):
super().__init__()
self.current_user = {"login": None}
self.geometry("458x360")
self.resizable(False, False)
bg_color = "#EEEEEE"
self.configure(bg=bg_color)
main_frame = tk.Frame(self, bg=bg_color)
main_frame.pack(padx=1, pady=1, fill='both', expand=True)
button_width = 20
button_height = 2
button_font = ("Arial", 12, "bold")
for i in range(7):
main_frame.grid_rowconfigure(i, weight=0)
main_frame.grid_columnconfigure(0, minsize=210, weight=0)
main_frame.grid_columnconfigure(1, minsize=235, weight=0)
# --- ЛЕВАЯ ПАНЕЛЬ ---
left_buttons = [
("Администратор", self._show_admin_menu),
("Менеджер базы", self._show_manager_menu),
("Эксперты", self._show_experts_menu),
("Проект", self._show_project_menu),
("Прогнозирование", self._show_forecast_menu)
]
for row_index, (btn_text, command_func) in enumerate(left_buttons):
btn = tk.Button(
main_frame,
text=btn_text,
font=button_font,
width=button_width,
height=button_height,
anchor="w",
command=command_func
)
btn.grid(row=row_index, column=0, padx=5, pady=5, sticky="ew")
# --- ПРАВАЯ ПАНЕЛЬ (СОВА) ---
right_box = tk.Frame(main_frame, bg=bg_color, width=235, height=300)
right_box.grid(row=0, column=1, rowspan=6, pady=5, sticky="nsew", padx=(0, 5))
right_box.grid_propagate(False)
right_box.columnconfigure(0, weight=1)
picture_frame = tk.Frame(right_box, bg="#d9ead3", highlightbackground="gray",
highlightthickness=1, width=225, height=160)
picture_frame.place(relx=0.5, y=5, anchor="n")
picture_frame.grid_propagate(False)
current_dir = os.path.dirname(os.path.abspath(__file__))
image_path = os.path.join(current_dir, "images", "Сова1.png")
try:
original_img = Image.open(image_path)
target_w, target_h = 215, 130
img_ratio = original_img.width / original_img.height
box_ratio = target_w / target_h
if img_ratio > box_ratio:
new_w, new_h = target_w, int(target_w / img_ratio)
else:
new_w, new_h = int(target_h * img_ratio), target_h
resized_img = original_img.resize((new_w, new_h), resample=Image.LANCZOS)
self.tk_image = ImageTk.PhotoImage(resized_img)
label = tk.Label(picture_frame, image=self.tk_image, bg="#d9ead3", borderwidth=0)
label.image = self.tk_image
label.place(relx=0.5, rely=0.5, anchor='center', width=new_w, height=new_h)
except Exception as e:
print(f"[FATAL IMAGE ERROR]: {e}")
canvas = tk.Canvas(picture_frame, width=225, height=160, bg="red", highlightthickness=0)
canvas.create_text(112, 80, text="IMG\nFAIL", fill="white")
canvas.place(x=0, y=0, relwidth=1, relheight=1)
spacer = tk.Frame(right_box, bg=bg_color, height=1)
spacer.grid(row=0, column=0, pady=(170, 10), sticky="ew")
upper_padding = tk.Frame(right_box, bg=bg_color, height=3)
upper_padding.grid(row=1, column=0)
btn6 = tk.Button(right_box, text="Руководство", font=button_font, width=button_width, height=button_height)
btn6.grid(row=2, column=0, ipadx=10, pady=(0, 5), sticky="ew")
btn7 = tk.Button(right_box, text="О программе", font=button_font, width=button_width, height=button_height)
btn7.grid(row=3, column=0, ipadx=10, pady=(5, 15), sticky="ew")
footer_frame = tk.Frame(main_frame, bg="#0078D7")
footer_frame.grid(row=6, column=0, columnspan=2, sticky="ew", pady=(10, 0))
footer_label = tk.Label(footer_frame, text="Copyright © Минск, НИИ, 2027 год", fg="#FF8C00",
bg="#0078D7", font=("Arial", 12, "bold"))
footer_label.pack(expand=True, ipady=4)
# --- КОНТЕЙНЕРЫ ДЛЯ ПОДМЕНЮ ---
self.menu_container = tk.Frame(main_frame, bg=bg_color)
self.menu_container.grid(row=0, column=0, rowspan=6, columnspan=2, sticky="nsew")
self.menu_container.grid_rowconfigure(0, weight=1)
self.menu_container.grid_columnconfigure(0, weight=1)
# Убираем старые заглушки, оставляем только Админа и Менеджера
self.admin_panel = None
self.manager_panel = None
self._current_menu_visible = None
# --- МЕТОД ВОЗВРАТА НА ГЛАВНЫЙ ЭКРАН ---
def _show_main_screen(self):
self._hide_current_menu()
self.menu_container.grid_remove()
self.grid_columnconfigure(0, minsize=210, weight=0)
self.grid_columnconfigure(1, minsize=235, weight=0)
def _hide_current_menu(self):
print("[LOG] Скрываем текущее меню...")
# Добавили удаление фасада Менеджера базы
if hasattr(self, 'manager_panel') and self.manager_panel is not None:
self.manager_panel.destroy()
self.manager_panel = None
# Оставили удаление фасада Администратора (он уже был)
if hasattr(self, 'admin_panel') and self.admin_panel is not None:
self.admin_panel.destroy()
self.admin_panel = None
# Очистка контейнера с серыми заглушками (на всякий случай)
for child in self.menu_container.winfo_children():
child.destroy()
self._current_menu_visible = None
def _show_admin_menu(self):
print("[LOG] Нажата кнопка 'Администратор'.")
self._hide_current_menu()
try:
from modules.administrator.admin_ui import AdminPanel
self.admin_panel = AdminPanel(self, current_user=self.current_user)
self.admin_panel.place(relx=0, rely=0, relwidth=1, relheight=1)
self.admin_panel.lift()
self.update_idletasks()
self._current_menu_visible = 'admin'
except Exception as e:
print(f"[FATAL ERROR] Не удалось создать панель администратора: {e}")
def _show_manager_menu(self):
print("[LOG] Нажата кнопка 'Менеджер базы'.")
# Прячем всё старое одним махом
self._hide_current_menu()
try:
from modules.manager.manager_ui import ManagerPanel
# Создаем панель ПРЯМО В ГЛАВНОМ ОКНЕ (self), а не в menu_container
self.manager_panel = ManagerPanel(self, current_user=self.current_user)
# Кладем её НАД ВСЕМ (поверх левой панели, правой панели и картинки)
self.manager_panel.place(relx=0, rely=0, relwidth=1, relheight=1)
# Принудительно просим поднять Z-index именно этой панели
self.manager_panel.lift()
# Заставляем Tkinter перерисоваться СЕЙЧАС
self.update_idletasks()
self._current_menu_visible = 'manager'
print("[LOG] Фасад Менеджера успешно размещен.")
except Exception as e:
# Теперь мы точно увидим ошибку, если она есть
print(f"[FATAL CRASH IN MANAGER MENU] {e}")
# Остальные кнопки пока просто закрывают оверлей (возвращают к сове)
def _show_experts_menu(self):
self._show_main_screen()
def _show_project_menu(self):
self._show_main_screen()
def _show_forecast_menu(self):
self._show_main_screen()
def _show_guide_menu(self):
self._show_main_screen()
def _show_about_menu(self):
self._show_main_screen()
def switch_user_radical(self):
root_win = self.winfo_toplevel()
from modules.utils.app_state import disable_app
disable_app(root_win)
if hasattr(self, 'current_user'):
self.current_user = {"login": None}
if hasattr(self, '_child_windows'):
self._child_windows.clear()
try:
root_win.destroy()
except Exception:
pass
import sys, subprocess, os, shutil
script_path = os.path.abspath("main.py")
gui_python = shutil.which("pythonw.exe")
if not gui_python:
current_dir = os.path.dirname(sys.executable)
potential_path = os.path.join(current_dir, "pythonw.exe")
gui_python = potential_path if os.path.exists(potential_path) else sys.executable
try:
subprocess.Popen([gui_python, script_path], shell=True, creationflags=subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0)
except Exception as e:
print(f"[FATAL] Не удалось запустить новую копию Dozor: {e}")
sys.exit(0)