fix: все замечания по UI, импортёр, словари, вкладка пользователей
- Добавлен dokogen/importer.py (импорт из Excel) - Созданы словари: ПДн, Субъекты, СЗИ (JSON) - Добавлена вкладка Пользователи (Должность, ФИО, доступ) - Исправлены: пустые поля номера/даты договора - Починен IS tab (названия не пропадают) - Кнопки Выбрать из списка для ПДн и СЗИ - Исправлен _apply_imported_data
This commit is contained in:
+338
-9
@@ -39,8 +39,8 @@ class DokoGenApp:
|
||||
)
|
||||
|
||||
# Переменные
|
||||
self.doc_number_var = tk.StringVar(value="001")
|
||||
self.doc_date_var = tk.StringVar(value=datetime.now().strftime("%d.%m.%Y"))
|
||||
self.doc_number_var = tk.StringVar(value="")
|
||||
self.doc_date_var = tk.StringVar(value="")
|
||||
self.templates_var = tk.StringVar()
|
||||
self.save_var = tk.StringVar()
|
||||
self.status_var = tk.StringVar(value="Готов")
|
||||
@@ -110,6 +110,7 @@ class DokoGenApp:
|
||||
self._build_company_tab()
|
||||
self._build_is_tab()
|
||||
self._build_commission_tab()
|
||||
self._build_users_tab()
|
||||
self._build_generation_tab()
|
||||
self._build_log_tab()
|
||||
|
||||
@@ -327,23 +328,32 @@ class DokoGenApp:
|
||||
# Список ПДн
|
||||
pd_tab = ttk.Frame(nb2)
|
||||
nb2.add(pd_tab, text="Перечень ПДн")
|
||||
pd_btn_frame = ttk.Frame(pd_tab)
|
||||
pd_btn_frame.pack(fill=tk.X)
|
||||
ttk.Button(pd_btn_frame, text="+ Добавить", command=self._add_pd_item).pack(side=tk.LEFT, padx=2)
|
||||
ttk.Button(pd_btn_frame, text="Выбрать из списка", command=self._select_from_pd_list).pack(side=tk.LEFT, padx=2)
|
||||
self.pd_listbox = tk.Listbox(pd_tab, height=6)
|
||||
self.pd_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||
ttk.Button(pd_tab, text="+ Добавить", command=self._add_pd_item).pack(pady=3)
|
||||
|
||||
# Субъекты ПДн
|
||||
subj_tab = ttk.Frame(nb2)
|
||||
nb2.add(subj_tab, text="Субъекты ПДн")
|
||||
subj_btn_frame = ttk.Frame(subj_tab)
|
||||
subj_btn_frame.pack(fill=tk.X)
|
||||
ttk.Button(subj_btn_frame, text="+ Добавить", command=self._add_pd_subjects_item).pack(side=tk.LEFT, padx=2)
|
||||
ttk.Button(subj_btn_frame, text="Выбрать из списка", command=self._select_from_subjects_list).pack(side=tk.LEFT, padx=2)
|
||||
self.pd_subjects_listbox = tk.Listbox(subj_tab, height=6)
|
||||
self.pd_subjects_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||
ttk.Button(subj_tab, text="+ Добавить", command=self._add_pd_subjects_item).pack(pady=3)
|
||||
|
||||
# СЗИ
|
||||
def_tab = ttk.Frame(nb2)
|
||||
nb2.add(def_tab, text="Средства защиты")
|
||||
def_btn_frame = ttk.Frame(def_tab)
|
||||
def_btn_frame.pack(fill=tk.X)
|
||||
ttk.Button(def_btn_frame, text="+ Добавить", command=self._add_defense_item).pack(side=tk.LEFT, padx=2)
|
||||
ttk.Button(def_btn_frame, text="Выбрать из списка", command=self._select_from_defense_list).pack(side=tk.LEFT, padx=2)
|
||||
self.defense_tools_listbox = tk.Listbox(def_tab, height=6)
|
||||
self.defense_tools_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||
ttk.Button(def_tab, text="+ Добавить", command=self._add_defense_item).pack(pady=3)
|
||||
|
||||
# Пользователи
|
||||
user_tab = ttk.Frame(nb2)
|
||||
@@ -390,6 +400,103 @@ class DokoGenApp:
|
||||
ttk.Button(frame, text="+ Добавить члена комиссии",
|
||||
command=self._add_commission_row_ui).pack(pady=10)
|
||||
|
||||
# ---------- ВКЛАДКА "ПОЛЬЗОВАТЕЛИ" ----------
|
||||
def _build_users_tab(self):
|
||||
tab = ttk.Frame(self.notebook)
|
||||
self.notebook.add(tab, text="Пользователи")
|
||||
|
||||
frame = ttk.Frame(tab, padding=10)
|
||||
frame.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
ttk.Label(frame, text="Сотрудники, имеющие доступ к ПДн",
|
||||
font=('Arial', 11, 'bold')).pack(anchor=tk.W, pady=(0, 10))
|
||||
|
||||
# Заголовки
|
||||
header = ttk.Frame(frame)
|
||||
header.pack(fill=tk.X)
|
||||
for w, col in [(25, "Должность"), (30, "ФИО"), (30, "Доступ к ИС")]:
|
||||
ttk.Label(header, text=col, font=('', 9, 'bold'),
|
||||
width=w, anchor=tk.W).pack(side=tk.LEFT, padx=2)
|
||||
|
||||
self._employees_frame = ttk.Frame(frame)
|
||||
self._employees_frame.pack(fill=tk.X, pady=5)
|
||||
|
||||
btn_frame = ttk.Frame(frame)
|
||||
btn_frame.pack(fill=tk.X, pady=5)
|
||||
ttk.Button(btn_frame, text="+ Добавить сотрудника",
|
||||
command=self._add_employee_row_ui).pack(side=tk.LEFT, padx=2)
|
||||
ttk.Button(btn_frame, text="- Удалить выбранного",
|
||||
command=self._delete_employee_selected).pack(side=tk.LEFT, padx=2)
|
||||
|
||||
# Счётчик
|
||||
self._emp_count_label = ttk.Label(frame, text="Добавлено: 0")
|
||||
self._emp_count_label.pack(anchor=tk.W, pady=5)
|
||||
|
||||
self._employee_entries = []
|
||||
# Загрузка существующих
|
||||
for emp in self.model.employees_access:
|
||||
self._add_employee_row(
|
||||
emp.get('position', ''),
|
||||
emp.get('fio', ''),
|
||||
emp.get('is_list', ''),
|
||||
)
|
||||
self._update_emp_count()
|
||||
|
||||
def _add_employee_row(self, position="", fio="", is_list=""):
|
||||
"""Добавляет одну строку сотрудника."""
|
||||
row = ttk.Frame(self._employees_frame)
|
||||
row.pack(fill=tk.X, pady=2)
|
||||
|
||||
pos_entry = ttk.Entry(row, width=25)
|
||||
pos_entry.insert(0, position)
|
||||
pos_entry.pack(side=tk.LEFT, padx=2)
|
||||
|
||||
fio_entry = ttk.Entry(row, width=30)
|
||||
fio_entry.insert(0, fio)
|
||||
fio_entry.pack(side=tk.LEFT, padx=2)
|
||||
|
||||
is_entry = ttk.Entry(row, width=30)
|
||||
is_entry.insert(0, is_list if isinstance(is_list, str) else ', '.join(is_list))
|
||||
is_entry.pack(side=tk.LEFT, padx=2)
|
||||
|
||||
del_btn = ttk.Button(row, text="✖", width=3,
|
||||
command=lambda: self._delete_employee_row(row))
|
||||
del_btn.pack(side=tk.LEFT, padx=5)
|
||||
|
||||
self._employee_entries.append((row, pos_entry, fio_entry, is_entry))
|
||||
self._update_emp_count()
|
||||
|
||||
def _add_employee_row_ui(self):
|
||||
self._add_employee_row()
|
||||
|
||||
def _delete_employee_row(self, row_frame):
|
||||
self._employee_entries = [e for e in self._employee_entries if e[0] != row_frame]
|
||||
row_frame.destroy()
|
||||
self._update_emp_count()
|
||||
|
||||
def _delete_employee_selected(self):
|
||||
# Удаляем последнюю строку, если есть
|
||||
if self._employee_entries:
|
||||
row_frame = self._employee_entries[-1][0]
|
||||
self._delete_employee_row(row_frame)
|
||||
|
||||
def _update_emp_count(self):
|
||||
count = len(self._employee_entries)
|
||||
if hasattr(self, '_emp_count_label'):
|
||||
self._emp_count_label.config(text=f"Добавлено: {count}")
|
||||
|
||||
def _collect_employees(self):
|
||||
"""Собирает данные сотрудников из GUI в модель."""
|
||||
self.model.employees_access.clear()
|
||||
for row, pos_e, fio_e, is_e in self._employee_entries:
|
||||
emp = {
|
||||
'position': pos_e.get().strip(),
|
||||
'fio': fio_e.get().strip(),
|
||||
'is_list': [s.strip() for s in is_e.get().split(',') if s.strip()],
|
||||
}
|
||||
if emp['position'] or emp['fio']:
|
||||
self.model.employees_access.append(emp)
|
||||
|
||||
# ---------- ВКЛАДКА "ГЕНЕРАЦИЯ" ----------
|
||||
def _build_generation_tab(self):
|
||||
tab = ttk.Frame(self.notebook)
|
||||
@@ -534,6 +641,19 @@ class DokoGenApp:
|
||||
self.is_listbox.selection_set(0)
|
||||
self._on_is_select()
|
||||
|
||||
# Пользователи
|
||||
if hasattr(self, '_employee_entries'):
|
||||
for row_frame, _, _, _ in self._employee_entries:
|
||||
row_frame.destroy()
|
||||
self._employee_entries.clear()
|
||||
for emp in c.employees_access:
|
||||
self._add_employee_row(
|
||||
emp.get('position', ''),
|
||||
emp.get('fio', ''),
|
||||
emp.get('is_list', []),
|
||||
)
|
||||
self._update_emp_count()
|
||||
|
||||
# Шаблоны
|
||||
self._on_templates_changed()
|
||||
|
||||
@@ -627,6 +747,8 @@ class DokoGenApp:
|
||||
lb.insert(tk.END, item)
|
||||
|
||||
def _add_is(self):
|
||||
# Сохраняем текущую ИС перед добавлением новой
|
||||
self._collect_is()
|
||||
name = simpledialog.askstring("Добавить ИС", "Введите наименование ИС:")
|
||||
if name:
|
||||
isys = InformationSystem(name=name)
|
||||
@@ -662,6 +784,7 @@ class DokoGenApp:
|
||||
def _delete_is(self):
|
||||
if self._current_is_idx is None:
|
||||
return
|
||||
self._collect_is()
|
||||
del self.model.information_systems[self._current_is_idx]
|
||||
self._current_is_idx = None
|
||||
self._refresh_is_listbox()
|
||||
@@ -694,6 +817,96 @@ class DokoGenApp:
|
||||
if sel:
|
||||
self.users_listbox.delete(sel[0])
|
||||
|
||||
# ==================== ВЫБОР ИЗ СПИСКА (словари) ====================
|
||||
def _select_from_dialog(self, title: str, items: list, listbox):
|
||||
"""Открывает окно с чекбоксами для выбора элементов из списка."""
|
||||
dialog = tk.Toplevel(self.root)
|
||||
dialog.title(title)
|
||||
dialog.geometry("400x500")
|
||||
dialog.transient(self.root)
|
||||
dialog.grab_set()
|
||||
|
||||
frame = ttk.Frame(dialog, padding=10)
|
||||
frame.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
ttk.Label(frame, text=title, font=('Arial', 10, 'bold')).pack(anchor=tk.W, pady=(0, 5))
|
||||
|
||||
# Уже выбранные элементы
|
||||
existing = set(listbox.get(0, tk.END))
|
||||
|
||||
canvas = tk.Canvas(frame, highlightthickness=0)
|
||||
scrollbar = ttk.Scrollbar(frame, orient=tk.VERTICAL, command=canvas.yview)
|
||||
check_frame = ttk.Frame(canvas)
|
||||
check_frame.bind("<Configure>", lambda e: canvas.configure(scrollregion=canvas.bbox("all")))
|
||||
canvas.create_window((0, 0), window=check_frame, anchor="nw")
|
||||
canvas.configure(yscrollcommand=scrollbar.set)
|
||||
canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
||||
|
||||
vars = {}
|
||||
for item in items:
|
||||
var = tk.BooleanVar(value=item in existing)
|
||||
cb = ttk.Checkbutton(check_frame, text=item, variable=var)
|
||||
cb.pack(anchor=tk.W, pady=1)
|
||||
vars[item] = var
|
||||
|
||||
btn_frame = ttk.Frame(dialog, padding=10)
|
||||
btn_frame.pack(fill=tk.X)
|
||||
|
||||
def _apply():
|
||||
selected = [item for item, var in vars.items() if var.get()]
|
||||
listbox.delete(0, tk.END)
|
||||
for item in selected:
|
||||
listbox.insert(tk.END, item)
|
||||
dialog.destroy()
|
||||
|
||||
def _select_all():
|
||||
for var in vars.values():
|
||||
var.set(True)
|
||||
|
||||
def _clear_all():
|
||||
for var in vars.values():
|
||||
var.set(False)
|
||||
|
||||
ttk.Button(btn_frame, text="Выбрать всё", command=_select_all).pack(side=tk.LEFT, padx=2)
|
||||
ttk.Button(btn_frame, text="Снять всё", command=_clear_all).pack(side=tk.LEFT, padx=2)
|
||||
ttk.Button(btn_frame, text="✅ Применить", command=_apply).pack(side=tk.RIGHT, padx=2)
|
||||
|
||||
dialog.wait_window()
|
||||
|
||||
def _select_from_pd_list(self):
|
||||
"""Выбор ПДн из JSON словаря."""
|
||||
import json
|
||||
dict_path = os.path.join(os.path.dirname(__file__), 'dictionaries', 'pd_items.json')
|
||||
try:
|
||||
with open(dict_path, 'r', encoding='utf-8') as f:
|
||||
items = json.load(f)
|
||||
self._select_from_dialog("Выбор персональных данных", items, self.pd_listbox)
|
||||
except Exception as e:
|
||||
messagebox.showerror("Ошибка", f"Не удалось загрузить словарь: {e}")
|
||||
|
||||
def _select_from_subjects_list(self):
|
||||
"""Выбор категорий субъектов из JSON словаря."""
|
||||
import json
|
||||
dict_path = os.path.join(os.path.dirname(__file__), 'dictionaries', 'pd_categories.json')
|
||||
try:
|
||||
with open(dict_path, 'r', encoding='utf-8') as f:
|
||||
items = json.load(f)
|
||||
self._select_from_dialog("Выбор категорий субъектов ПДн", items, self.pd_subjects_listbox)
|
||||
except Exception as e:
|
||||
messagebox.showerror("Ошибка", f"Не удалось загрузить словарь: {e}")
|
||||
|
||||
def _select_from_defense_list(self):
|
||||
"""Выбор СЗИ из JSON словаря."""
|
||||
import json
|
||||
dict_path = os.path.join(os.path.dirname(__file__), 'dictionaries', 'defense_tools.json')
|
||||
try:
|
||||
with open(dict_path, 'r', encoding='utf-8') as f:
|
||||
items = json.load(f)
|
||||
self._select_from_dialog("Выбор средств защиты информации", items, self.defense_tools_listbox)
|
||||
except Exception as e:
|
||||
messagebox.showerror("Ошибка", f"Не удалось загрузить словарь: {e}")
|
||||
|
||||
# ==================== ШАБЛОНЫ ====================
|
||||
def select_templates(self):
|
||||
folder = filedialog.askdirectory(title="Папка шаблонов")
|
||||
@@ -757,6 +970,7 @@ class DokoGenApp:
|
||||
self._collect_company()
|
||||
self._collect_commission()
|
||||
self._collect_is()
|
||||
self._collect_employees()
|
||||
|
||||
# Проверка
|
||||
if not self._validate_before_generate():
|
||||
@@ -868,22 +1082,135 @@ class DokoGenApp:
|
||||
|
||||
def _apply_imported_data(self, data: dict):
|
||||
def _apply():
|
||||
# Название организации
|
||||
if data.get('fullName'):
|
||||
self.company_name_entry.delete(0, tk.END)
|
||||
self.company_name_entry.insert(0, data['fullName'])
|
||||
self.model.full_name = data['fullName']
|
||||
if data.get('shortName'):
|
||||
self.short_name_entry.delete(0, tk.END)
|
||||
self.short_name_entry.insert(0, data['shortName'])
|
||||
for key, entry in self._company_entries.items():
|
||||
val = data.get(key) or data.get(key.replace('_', '')) or ''
|
||||
self.model.short_name = data['shortName']
|
||||
|
||||
# Адрес и реквизиты (маппинг camelCase → snake_case)
|
||||
addr_map = {
|
||||
'address': 'addressLegal',
|
||||
'city_name': None,
|
||||
'inn': 'inn',
|
||||
'kpp': 'kpp',
|
||||
'ogrn': 'ogrn',
|
||||
'ogrn_date': 'ogrnDate',
|
||||
}
|
||||
for entry_key, data_key in addr_map.items():
|
||||
entry = self._company_entries.get(entry_key)
|
||||
if not entry:
|
||||
continue
|
||||
val = ''
|
||||
if data_key and data.get(data_key):
|
||||
val = data[data_key]
|
||||
elif data.get(entry_key):
|
||||
val = data[entry_key]
|
||||
if val:
|
||||
entry.delete(0, tk.END)
|
||||
entry.insert(0, val)
|
||||
for attr, entry in self._official_entries.items():
|
||||
val = data.get(attr) or ''
|
||||
|
||||
# Должностные лица (маппинг)
|
||||
officials_map = {
|
||||
'chief_position': 'chiefPosition',
|
||||
'chief_fio': 'chiefFio',
|
||||
'responsible_position': 'responsiblePosition',
|
||||
'responsible_fio': 'responsibleFio',
|
||||
'admin_position': ('administratorPosition', 'ispdnPosition'),
|
||||
'admin_fio': ('administratorFio', 'ispdnFio'),
|
||||
}
|
||||
for entry_key, data_keys in officials_map.items():
|
||||
entry = self._official_entries.get(entry_key)
|
||||
if not entry:
|
||||
continue
|
||||
val = ''
|
||||
if isinstance(data_keys, tuple):
|
||||
for dk in data_keys:
|
||||
if data.get(dk):
|
||||
val = data[dk]
|
||||
break
|
||||
elif data.get(data_keys):
|
||||
val = data[data_keys]
|
||||
if val:
|
||||
entry.delete(0, tk.END)
|
||||
entry.insert(0, val)
|
||||
|
||||
# Номер и дата документа
|
||||
if data.get('contractNumber'):
|
||||
self.doc_number_var.set(data['contractNumber'])
|
||||
if data.get('contractDate'):
|
||||
self.doc_date_var.set(data['contractDate'])
|
||||
|
||||
# Информационные системы
|
||||
if data.get('informationSystems'):
|
||||
self.model.information_systems.clear()
|
||||
for is_data in data['informationSystems']:
|
||||
isys = InformationSystem(
|
||||
name=is_data.get('name', ''),
|
||||
description=is_data.get('description', ''),
|
||||
software=is_data.get('software', ''),
|
||||
is_internet=is_data.get('isInternet', False),
|
||||
pd_count=is_data.get('personalDataCount', 'менее 100 000'),
|
||||
personal_data_list=is_data.get('personalDataList', []),
|
||||
pd_subjects_list=is_data.get('pd_subjects_list', []),
|
||||
defense_tools_list=is_data.get('defense_tools', []),
|
||||
users_list=is_data.get('usersList', []),
|
||||
personal_data_category=is_data.get('personalDataCategory', []),
|
||||
processing_modes=is_data.get('processing_mode', ''),
|
||||
purpose=is_data.get('description', ''),
|
||||
room=is_data.get('room', ''),
|
||||
)
|
||||
self.model.information_systems.append(isys)
|
||||
self._refresh_is_listbox()
|
||||
if self.model.information_systems:
|
||||
self.is_listbox.selection_set(0)
|
||||
self._on_is_select()
|
||||
|
||||
# Комиссия
|
||||
if data.get('commission'):
|
||||
self.model.commission.clear()
|
||||
# Очистим UI
|
||||
for row_frame, _, _, _ in self.commission_entries:
|
||||
row_frame.destroy()
|
||||
self.commission_entries.clear()
|
||||
for m in data['commission']:
|
||||
self.model.commission.append(
|
||||
CommissionMember(
|
||||
role=m.get('role', 'Член комиссии'),
|
||||
position=m.get('position', ''),
|
||||
fio=m.get('fio', ''),
|
||||
)
|
||||
)
|
||||
self._add_commission_row(
|
||||
m.get('role', 'Член комиссии'),
|
||||
m.get('position', ''),
|
||||
m.get('fio', ''),
|
||||
)
|
||||
|
||||
# Сотрудники с доступом
|
||||
if data.get('employeesAccess'):
|
||||
self.model.employees_access.clear()
|
||||
if hasattr(self, '_employee_entries'):
|
||||
for row_frame, _, _, _ in self._employee_entries:
|
||||
row_frame.destroy()
|
||||
self._employee_entries.clear()
|
||||
for emp in data['employeesAccess']:
|
||||
self.model.employees_access.append({
|
||||
'position': emp.get('position', ''),
|
||||
'fio': emp.get('fio', ''),
|
||||
'is_list': emp.get('is_list', []),
|
||||
})
|
||||
self._add_employee_row(
|
||||
emp.get('position', ''),
|
||||
emp.get('fio', ''),
|
||||
emp.get('is_list', []),
|
||||
)
|
||||
self._update_emp_count()
|
||||
|
||||
self.log("✅ Импорт применён")
|
||||
self.root.after(0, _apply)
|
||||
|
||||
@@ -927,6 +1254,7 @@ class DokoGenApp:
|
||||
self._collect_company()
|
||||
self._collect_commission()
|
||||
self._collect_is()
|
||||
self._collect_employees()
|
||||
try:
|
||||
with open(filename, 'w', encoding='utf-8') as f:
|
||||
json.dump(self.model.to_dict(), f, ensure_ascii=False, indent=2)
|
||||
@@ -951,6 +1279,7 @@ class DokoGenApp:
|
||||
self._collect_company()
|
||||
self._collect_commission()
|
||||
self._collect_is()
|
||||
self._collect_employees()
|
||||
try:
|
||||
os.makedirs(os.path.dirname(self._autosave_path), exist_ok=True)
|
||||
with open(self._autosave_path, 'w', encoding='utf-8') as f:
|
||||
|
||||
Reference in New Issue
Block a user