fix: 8 багов — город, IS поля, ПО, авторасчёт, скролл, прогресc, комиссия, замены
This commit is contained in:
+153
-21
@@ -201,7 +201,7 @@ class DokoGenApp:
|
||||
|
||||
addr_fields = [
|
||||
("Юридический адрес:", "address"),
|
||||
("Город:", "city_name"),
|
||||
("Город (заполняется автоматически):", "city_name"),
|
||||
("ИНН:", "inn"),
|
||||
("КПП:", "kpp"),
|
||||
("ОГРН:", "ogrn"),
|
||||
@@ -212,6 +212,12 @@ class DokoGenApp:
|
||||
entry = ttk.Entry(f2, width=50)
|
||||
entry.grid(row=i, column=1, padx=10, sticky=tk.EW, pady=1)
|
||||
self._company_entries[key] = entry
|
||||
|
||||
# Привязка автозаполнения города из адреса
|
||||
addr_entry = self._company_entries.get('address')
|
||||
if addr_entry:
|
||||
addr_entry.bind('<KeyRelease>', self._on_address_changed)
|
||||
|
||||
f2.columnconfigure(1, weight=1)
|
||||
|
||||
# --- Должностные лица ---
|
||||
@@ -278,13 +284,9 @@ class DokoGenApp:
|
||||
self.is_description_entry = ttk.Entry(fields_frame, width=55)
|
||||
self.is_description_entry.grid(row=1, column=1, padx=10, sticky=tk.EW)
|
||||
|
||||
ttk.Label(fields_frame, text="ПО:").grid(row=2, column=0, sticky=tk.W, pady=2)
|
||||
self.is_software_entry = ttk.Entry(fields_frame, width=55)
|
||||
self.is_software_entry.grid(row=2, column=1, padx=10, sticky=tk.EW)
|
||||
|
||||
# Чекбоксы
|
||||
chk_frame = ttk.Frame(fields_frame)
|
||||
chk_frame.grid(row=3, column=1, sticky=tk.W, pady=5)
|
||||
chk_frame.grid(row=2, column=1, sticky=tk.W, pady=5)
|
||||
self.is_lan_var = tk.BooleanVar()
|
||||
ttk.Checkbutton(chk_frame, text="ЛВС", variable=self.is_lan_var).pack(side=tk.LEFT, padx=5)
|
||||
self.is_internet_var = tk.BooleanVar()
|
||||
@@ -292,33 +294,42 @@ class DokoGenApp:
|
||||
|
||||
# Тип угроз
|
||||
self.is_threat_var = tk.StringVar(value="3")
|
||||
ttk.Label(fields_frame, text="Тип угроз:").grid(row=4, column=0, sticky=tk.W, pady=2)
|
||||
ttk.Label(fields_frame, text="Тип угроз:").grid(row=3, column=0, sticky=tk.W, pady=2)
|
||||
threat_frame = ttk.Frame(fields_frame)
|
||||
threat_frame.grid(row=4, column=1, sticky=tk.W, padx=10)
|
||||
threat_frame.grid(row=3, column=1, sticky=tk.W, padx=10)
|
||||
for val, text in [("1", "1 (НДВ в СПО)"), ("2", "2 (НДВ в ППО)"), ("3", "3 (НДВ нет)")]:
|
||||
ttk.Radiobutton(threat_frame, text=text, variable=self.is_threat_var,
|
||||
value=val).pack(side=tk.LEFT, padx=3)
|
||||
|
||||
# Уровень защищённости
|
||||
self.is_defence_var = tk.StringVar(value="4")
|
||||
ttk.Label(fields_frame, text="Ур. защищённости:").grid(row=5, column=0, sticky=tk.W, pady=2)
|
||||
ttk.Label(fields_frame, text="Ур. защищённости:").grid(row=4, column=0, sticky=tk.W, pady=2)
|
||||
def_frame = ttk.Frame(fields_frame)
|
||||
def_frame.grid(row=5, column=1, sticky=tk.W, padx=10)
|
||||
def_frame.grid(row=4, column=1, sticky=tk.W, padx=10)
|
||||
for lvl in ["1", "2", "3", "4"]:
|
||||
ttk.Radiobutton(def_frame, text=f"{lvl} ур.",
|
||||
variable=self.is_defence_var, value=lvl).pack(side=tk.LEFT, padx=3)
|
||||
ttk.Button(def_frame, text="Рассчитать автоматически",
|
||||
command=self._auto_calc_defence_level).pack(side=tk.LEFT, padx=10)
|
||||
|
||||
# Категории ПДн
|
||||
ttk.Label(fields_frame, text="Категории ПДн:").grid(row=6, column=0, sticky=tk.NW, pady=2)
|
||||
ttk.Label(fields_frame, text="Категории ПДн:").grid(row=5, column=0, sticky=tk.NW, pady=2)
|
||||
cat_frame = ttk.Frame(fields_frame)
|
||||
cat_frame.grid(row=6, column=1, sticky=tk.W, padx=10)
|
||||
cat_frame.grid(row=5, column=1, sticky=tk.W, padx=10)
|
||||
self.category_vars = []
|
||||
for cat in ["специальные", "биометрические", "общедоступные", "иные"]:
|
||||
var = tk.BooleanVar()
|
||||
var.trace_add('write', lambda *_: self._on_categories_or_count_changed())
|
||||
cb = ttk.Checkbutton(cat_frame, text=cat, variable=var)
|
||||
cb.pack(anchor=tk.W)
|
||||
self.category_vars.append((cat, var))
|
||||
|
||||
# Количество записей ПДн
|
||||
ttk.Label(fields_frame, text="Кол-во записей:").grid(row=6, column=0, sticky=tk.W, pady=2)
|
||||
self.is_pd_count_entry = ttk.Entry(fields_frame, width=30)
|
||||
self.is_pd_count_entry.grid(row=6, column=1, padx=10, sticky=tk.W)
|
||||
self.is_pd_count_entry.bind('<KeyRelease>', lambda e: self._on_categories_or_count_changed())
|
||||
|
||||
fields_frame.columnconfigure(1, weight=1)
|
||||
|
||||
# Вложенные списки
|
||||
@@ -405,7 +416,17 @@ class DokoGenApp:
|
||||
tab = ttk.Frame(self.notebook)
|
||||
self.notebook.add(tab, text="Пользователи")
|
||||
|
||||
frame = ttk.Frame(tab, padding=10)
|
||||
canvas = tk.Canvas(tab, highlightthickness=0)
|
||||
scrollbar = ttk.Scrollbar(tab, orient=tk.VERTICAL, command=canvas.yview)
|
||||
scrollable = ttk.Frame(canvas)
|
||||
scrollable.bind("<Configure>",
|
||||
lambda e: canvas.configure(scrollregion=canvas.bbox("all")))
|
||||
canvas.create_window((0, 0), window=scrollable, 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)
|
||||
|
||||
frame = ttk.Frame(scrollable, padding=10)
|
||||
frame.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
ttk.Label(frame, text="Сотрудники, имеющие доступ к ПДн",
|
||||
@@ -572,13 +593,14 @@ class DokoGenApp:
|
||||
isys = self.model.information_systems[self._current_is_idx]
|
||||
isys.name = self.is_name_entry.get().strip()
|
||||
isys.description = self.is_description_entry.get().strip()
|
||||
isys.software = self.is_software_entry.get().strip()
|
||||
isys.is_local_network = self.is_lan_var.get()
|
||||
isys.is_internet = self.is_internet_var.get()
|
||||
isys.threat_type = self.is_threat_var.get()
|
||||
isys.defence_level = self.is_defence_var.get()
|
||||
cats = [c for c, v in self.category_vars if v.get()]
|
||||
isys.personal_data_category = cats
|
||||
if hasattr(self, 'is_pd_count_entry'):
|
||||
isys.pd_count = self.is_pd_count_entry.get().strip() or 'менее 100 000'
|
||||
# Списки
|
||||
isys.personal_data_list = list(self.pd_listbox.get(0, tk.END))
|
||||
isys.pd_subjects_list = list(self.pd_subjects_listbox.get(0, tk.END))
|
||||
@@ -657,6 +679,92 @@ class DokoGenApp:
|
||||
# Шаблоны
|
||||
self._on_templates_changed()
|
||||
|
||||
# Автоопределение города из адреса после загрузки
|
||||
self._on_address_changed()
|
||||
|
||||
# ==================== ГОРОД ИЗ АДРЕСА ====================
|
||||
def _extract_city_from_address(self, address: str) -> str:
|
||||
"""Извлекает название города из адреса (простая эвристика)."""
|
||||
if not address:
|
||||
return ''
|
||||
# Пробуем найти "г. Город"
|
||||
import re
|
||||
m = re.search(r'\bг\.\s*([А-ЯЁ][а-яёA-Za-z-]+(?:\s+[А-ЯЁ][а-яёA-Za-z-]+)*)', address)
|
||||
if m:
|
||||
return m.group(1).strip()
|
||||
# Пробуем найти "город Город"
|
||||
m = re.search(r'\bгород\s+([А-ЯЁ][а-яёA-Za-z-]+(?:\s+[А-ЯЁ][а-яёA-Za-z-]+)*)', address)
|
||||
if m:
|
||||
return m.group(1).strip()
|
||||
# Пробуем первую часть до запятой, если там известный город
|
||||
parts = [p.strip() for p in address.split(',')]
|
||||
if parts:
|
||||
first = parts[0]
|
||||
# Если первая часть — короткая и не похожа на улицу/дом
|
||||
if len(first) <= 25 and first and not first.startswith(('ул', 'пр', 'пер', 'шоссе')):
|
||||
# Проверяем, что это похоже на город
|
||||
city_keywords = ['ск', 'цк', 'бург', 'град', 'полис', 'поль', 'стан', 'горск', 'чинск', 'янск', 'овск']
|
||||
if any(first.lower().endswith(kw) for kw in city_keywords):
|
||||
return first
|
||||
return ''
|
||||
|
||||
def _on_address_changed(self, event=None):
|
||||
"""Обработчик изменения адреса — автозаполняет город."""
|
||||
addr_entry = self._company_entries.get('address')
|
||||
city_entry = self._company_entries.get('city_name')
|
||||
if not addr_entry or not city_entry:
|
||||
return
|
||||
address = addr_entry.get().strip()
|
||||
if not address:
|
||||
return
|
||||
current_city = city_entry.get().strip()
|
||||
if current_city:
|
||||
return # Не перезаписываем, если уже заполнен
|
||||
city = self._extract_city_from_address(address)
|
||||
if city:
|
||||
city_entry.delete(0, tk.END)
|
||||
city_entry.insert(0, city)
|
||||
self.log(f"🏙 Город определён: {city}")
|
||||
|
||||
# ==================== АВТОРАСЧЁТ УРОВНЯ ЗАЩИЩЁННОСТИ ====================
|
||||
def _auto_calc_defence_level(self):
|
||||
"""Автоматический расчёт уровня защищённости на основе категорий ПДн и кол-ва записей."""
|
||||
if self._current_is_idx is None:
|
||||
return
|
||||
|
||||
# Считываем категории
|
||||
categories = [cat for cat, var in self.category_vars if var.get()]
|
||||
pd_count_text = self.is_pd_count_entry.get().strip() if hasattr(self, 'is_pd_count_entry') else ''
|
||||
|
||||
# Определяем базовый уровень (4 — самый низкий, по умолчанию)
|
||||
level = 4
|
||||
|
||||
# Специальные или биометрические → минимум 3 уровень
|
||||
has_special = any(c in categories for c in ['специальные', 'биометрические'])
|
||||
if has_special:
|
||||
level = 3
|
||||
|
||||
# Больше 100 000 записей → повышаем уровень на 1 (уровень становится строже)
|
||||
if pd_count_text:
|
||||
# Парсим число
|
||||
import re
|
||||
nums = re.findall(r'\d+', pd_count_text.replace(' ', ''))
|
||||
if nums:
|
||||
count = int(nums[0])
|
||||
if count > 100000:
|
||||
level = max(1, level - 1) # Уровень повышается (число уменьшается)
|
||||
# Также проверяем текстовые варианты
|
||||
if 'более' in pd_count_text.lower() or 'свыше' in pd_count_text.lower():
|
||||
level = max(1, level - 1)
|
||||
|
||||
self.is_defence_var.set(str(level))
|
||||
self.log(f"🛡 Уровень защищённости автоматически рассчитан: {level}")
|
||||
|
||||
def _on_categories_or_count_changed(self):
|
||||
"""Обработчик изменения категорий ПДн или количества записей."""
|
||||
if self._current_is_idx is not None:
|
||||
self._auto_calc_defence_level()
|
||||
|
||||
# ==================== КОМИССИЯ ====================
|
||||
def _add_commission_row(self, role="Член комиссии", position="", fio=""):
|
||||
row = ttk.Frame(self._comm_members_frame)
|
||||
@@ -665,7 +773,7 @@ class DokoGenApp:
|
||||
role_var = tk.StringVar(value=role)
|
||||
role_cb = ttk.Combobox(row, textvariable=role_var, state='readonly', width=22,
|
||||
values=["Председатель комиссии", "Секретарь комиссии",
|
||||
"Член комиссии", "Заместитель председателя"])
|
||||
"Член комиссии"])
|
||||
role_cb.pack(side=tk.LEFT, padx=2)
|
||||
|
||||
pos_entry = ttk.Entry(row, width=30)
|
||||
@@ -702,8 +810,6 @@ class DokoGenApp:
|
||||
|
||||
def _set_is_edit_state(self, enabled):
|
||||
state = 'normal' if enabled else 'disabled'
|
||||
for w in [self.is_name_entry, self.is_description_entry, self.is_software_entry]:
|
||||
w.config(state=state)
|
||||
self.is_lan_var.set(False)
|
||||
self.is_internet_var.set(False)
|
||||
|
||||
@@ -726,14 +832,16 @@ class DokoGenApp:
|
||||
self.is_name_entry.insert(0, isys.name or "")
|
||||
self.is_description_entry.delete(0, tk.END)
|
||||
self.is_description_entry.insert(0, isys.description or "")
|
||||
self.is_software_entry.delete(0, tk.END)
|
||||
self.is_software_entry.insert(0, isys.software or "")
|
||||
self.is_lan_var.set(isys.is_local_network)
|
||||
self.is_internet_var.set(isys.is_internet)
|
||||
self.is_threat_var.set(isys.threat_type or "3")
|
||||
self.is_defence_var.set(isys.defence_level or "4")
|
||||
for cat, var in self.category_vars:
|
||||
var.set(cat in isys.personal_data_category)
|
||||
# Заполняем кол-во записей ПДн
|
||||
if hasattr(self, 'is_pd_count_entry'):
|
||||
self.is_pd_count_entry.delete(0, tk.END)
|
||||
self.is_pd_count_entry.insert(0, isys.pd_count or 'менее 100 000')
|
||||
|
||||
# Списки
|
||||
for lb, items in [
|
||||
@@ -765,7 +873,6 @@ class DokoGenApp:
|
||||
copy = InformationSystem(
|
||||
name=src.name + " (копия)",
|
||||
description=src.description,
|
||||
software=src.software,
|
||||
is_local_network=src.is_local_network,
|
||||
is_internet=src.is_internet,
|
||||
defence_level=src.defence_level,
|
||||
@@ -978,6 +1085,27 @@ class DokoGenApp:
|
||||
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
# Окно прогресса
|
||||
progress_win = tk.Toplevel(self.root)
|
||||
progress_win.title("Генерация документов")
|
||||
progress_win.geometry("350x100")
|
||||
progress_win.transient(self.root)
|
||||
progress_win.grab_set()
|
||||
progress_win.resizable(False, False)
|
||||
ttk.Label(progress_win, text="Генерация документов...",
|
||||
font=('Arial', 10)).pack(pady=(15, 5))
|
||||
pb = ttk.Progressbar(progress_win, mode='indeterminate', length=300)
|
||||
pb.pack(pady=5)
|
||||
pb.start()
|
||||
|
||||
def _close_progress():
|
||||
try:
|
||||
pb.stop()
|
||||
progress_win.grab_release()
|
||||
progress_win.destroy()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Запускаем в отдельном потоке
|
||||
def _run():
|
||||
try:
|
||||
@@ -1006,12 +1134,14 @@ class DokoGenApp:
|
||||
self.log(f"❌ {fname}: {e}")
|
||||
|
||||
self.log(f"✅ Готово: {len(results)} из {len(selected)}")
|
||||
self.root.after(0, _close_progress)
|
||||
self.root.after(0, lambda: messagebox.showinfo(
|
||||
"Готово", f"Сгенерировано {len(results)} документов\n"
|
||||
f"Папка: {output_dir}"
|
||||
))
|
||||
except Exception as e:
|
||||
self.log(f"❌ Ошибка: {e}")
|
||||
self.root.after(0, _close_progress)
|
||||
self.root.after(0, lambda: messagebox.showerror("Ошибка", str(e)))
|
||||
|
||||
threading.Thread(target=_run, daemon=True).start()
|
||||
@@ -1114,6 +1244,9 @@ class DokoGenApp:
|
||||
entry.delete(0, tk.END)
|
||||
entry.insert(0, val)
|
||||
|
||||
# Автоопределение города из адреса после импорта
|
||||
self._on_address_changed()
|
||||
|
||||
# Должностные лица (маппинг)
|
||||
officials_map = {
|
||||
'chief_position': 'chiefPosition',
|
||||
@@ -1152,7 +1285,6 @@ class DokoGenApp:
|
||||
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', []),
|
||||
|
||||
Reference in New Issue
Block a user