fix: все замечания по UI, импортёр, словари, вкладка пользователей

- Добавлен dokogen/importer.py (импорт из Excel)
- Созданы словари: ПДн, Субъекты, СЗИ (JSON)
- Добавлена вкладка Пользователи (Должность, ФИО, доступ)
- Исправлены: пустые поля номера/даты договора
- Починен IS tab (названия не пропадают)
- Кнопки Выбрать из списка для ПДн и СЗИ
- Исправлен _apply_imported_data
This commit is contained in:
2026-07-30 14:06:19 +04:00
parent 5315d92b59
commit 7eb736bcbf
8 changed files with 830 additions and 10 deletions
+338 -9
View File
@@ -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: