fix: 8 багов — город, IS поля, ПО, авторасчёт, скролл, прогресc, комиссия, замены
This commit is contained in:
+56
-8
@@ -57,16 +57,19 @@ def process_template(
|
|||||||
buf.seek(0)
|
buf.seek(0)
|
||||||
doc = Document(buf)
|
doc = Document(buf)
|
||||||
|
|
||||||
# 5. Плоская замена переменных (2 прохода)
|
# 5. Плоская замена переменных (3 прохода)
|
||||||
for _ in range(2):
|
for _ in range(3):
|
||||||
_replace_in_paragraphs(doc.paragraphs, replacements)
|
_replace_in_paragraphs(doc.paragraphs, replacements)
|
||||||
_replace_in_tables(doc.tables, replacements)
|
_replace_in_tables(doc.tables, replacements)
|
||||||
_replace_in_headers_footers(doc, replacements)
|
_replace_in_headers_footers(doc, replacements)
|
||||||
|
|
||||||
# 6. Подсветка незаполненных
|
# 6. Логирование незаменённых переменных
|
||||||
|
_log_unmatched(doc, replacements, log_func if log_func else lambda x: None)
|
||||||
|
|
||||||
|
# 7. Подсветка незаполненных
|
||||||
_highlight_unmatched(doc)
|
_highlight_unmatched(doc)
|
||||||
|
|
||||||
# 7. Сохранение
|
# 8. Сохранение
|
||||||
doc.save(output_path)
|
doc.save(output_path)
|
||||||
|
|
||||||
if log_func:
|
if log_func:
|
||||||
@@ -338,12 +341,57 @@ def _find_element_bounds(xml_str: str, pos: int):
|
|||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== ЛОГИРОВАНИЕ НЕЗАМЕНЁННЫХ ====================
|
||||||
|
|
||||||
|
UNMATCHED_PATTERN = re.compile(r'\{\{[^}]+\}\}|<[A-Za-z0-9_]+>')
|
||||||
|
|
||||||
|
|
||||||
|
def _log_unmatched(doc, replacements: Dict[str, str], log_func):
|
||||||
|
"""Логирует оставшиеся незаменённые переменные."""
|
||||||
|
unmatched = set()
|
||||||
|
|
||||||
|
def _scan_text(text):
|
||||||
|
for m in UNMATCHED_PATTERN.finditer(text):
|
||||||
|
var = m.group(0)
|
||||||
|
# Проверяем, не является ли это уже обработанной переменной
|
||||||
|
if var.startswith('{{'):
|
||||||
|
if var not in replacements:
|
||||||
|
unmatched.add(var)
|
||||||
|
elif var.startswith('<'):
|
||||||
|
if var not in replacements:
|
||||||
|
unmatched.add(var)
|
||||||
|
|
||||||
|
for para in doc.paragraphs:
|
||||||
|
_scan_text(para.text)
|
||||||
|
for table in doc.tables:
|
||||||
|
for row in table.rows:
|
||||||
|
for cell in row.cells:
|
||||||
|
for para in cell.paragraphs:
|
||||||
|
_scan_text(para.text)
|
||||||
|
for section in doc.sections:
|
||||||
|
for hf in [section.header, section.footer,
|
||||||
|
section.first_page_header, section.first_page_footer]:
|
||||||
|
if hf:
|
||||||
|
for para in hf.paragraphs:
|
||||||
|
_scan_text(para.text)
|
||||||
|
|
||||||
|
if unmatched:
|
||||||
|
log_func(f"⚠️ Незаменённые переменные ({len(unmatched)}): {', '.join(sorted(unmatched)[:20])}")
|
||||||
|
if len(unmatched) > 20:
|
||||||
|
log_func(f" ... и ещё {len(unmatched) - 20}")
|
||||||
|
else:
|
||||||
|
log_func("✅ Все переменные успешно заменены")
|
||||||
|
|
||||||
|
|
||||||
# ==================== ПОДСВЕТКА НЕЗАПОЛНЕННЫХ ====================
|
# ==================== ПОДСВЕТКА НЕЗАПОЛНЕННЫХ ====================
|
||||||
|
|
||||||
def _highlight_unmatched(doc):
|
def _highlight_unmatched(doc):
|
||||||
"""Подсвечивает незаполненные переменные красным."""
|
"""Подсвечивает незаполненные переменные красным ({{...}} и старые <...>)."""
|
||||||
ns = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'
|
ns = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'
|
||||||
|
|
||||||
|
def _has_unmatched(text):
|
||||||
|
return bool(UNMATCHED_PATTERN.search(text))
|
||||||
|
|
||||||
def _highlight_run(run):
|
def _highlight_run(run):
|
||||||
rpr = run._element.find(f'{{{ns}}}rPr')
|
rpr = run._element.find(f'{{{ns}}}rPr')
|
||||||
if rpr is None:
|
if rpr is None:
|
||||||
@@ -358,7 +406,7 @@ def _highlight_unmatched(doc):
|
|||||||
|
|
||||||
for para in doc.paragraphs:
|
for para in doc.paragraphs:
|
||||||
for run in para.runs:
|
for run in para.runs:
|
||||||
if '{{' in run.text and '}}' in run.text:
|
if _has_unmatched(run.text):
|
||||||
_highlight_run(run)
|
_highlight_run(run)
|
||||||
|
|
||||||
for table in doc.tables:
|
for table in doc.tables:
|
||||||
@@ -366,7 +414,7 @@ def _highlight_unmatched(doc):
|
|||||||
for cell in row.cells:
|
for cell in row.cells:
|
||||||
for para in cell.paragraphs:
|
for para in cell.paragraphs:
|
||||||
for run in para.runs:
|
for run in para.runs:
|
||||||
if '{{' in run.text and '}}' in run.text:
|
if _has_unmatched(run.text):
|
||||||
_highlight_run(run)
|
_highlight_run(run)
|
||||||
|
|
||||||
for section in doc.sections:
|
for section in doc.sections:
|
||||||
@@ -374,5 +422,5 @@ def _highlight_unmatched(doc):
|
|||||||
if hf:
|
if hf:
|
||||||
for para in hf.paragraphs:
|
for para in hf.paragraphs:
|
||||||
for run in para.runs:
|
for run in para.runs:
|
||||||
if '{{' in run.text and '}}' in run.text:
|
if _has_unmatched(run.text):
|
||||||
_highlight_run(run)
|
_highlight_run(run)
|
||||||
|
|||||||
+1
-6
@@ -173,7 +173,7 @@ def _import_152_is_list(wb, data, log_fn):
|
|||||||
|
|
||||||
is_obj = {
|
is_obj = {
|
||||||
'name': '', 'description': '', 'room': '', 'employee': '', 'position': '',
|
'name': '', 'description': '', 'room': '', 'employee': '', 'position': '',
|
||||||
'software': '', 'personalDataList': [], 'pd_subjects_list': [], 'pd_actions': [],
|
'personalDataList': [], 'pd_subjects_list': [], 'pd_actions': [],
|
||||||
'usersList': [], 'isInternet': False, 'personalDataCount': 'менее 100 000',
|
'usersList': [], 'isInternet': False, 'personalDataCount': 'менее 100 000',
|
||||||
'defense_tools': [], 'processing_mode': '',
|
'defense_tools': [], 'processing_mode': '',
|
||||||
}
|
}
|
||||||
@@ -232,8 +232,6 @@ def _import_152_is_list(wb, data, log_fn):
|
|||||||
pass
|
pass
|
||||||
elif 'номер кабинета' in field_lower:
|
elif 'номер кабинета' in field_lower:
|
||||||
is_obj['room'] = val_b
|
is_obj['room'] = val_b
|
||||||
elif 'названия программ' in field_lower or ('программ' in field_lower and 'перечень' not in field_lower):
|
|
||||||
is_obj['software'] = val_b
|
|
||||||
elif 'значение' in field_lower or 'примечание' in field_lower:
|
elif 'значение' in field_lower or 'примечание' in field_lower:
|
||||||
continue
|
continue
|
||||||
elif val_b and 'выбрать' not in field_lower:
|
elif val_b and 'выбрать' not in field_lower:
|
||||||
@@ -302,9 +300,6 @@ def _import_152_is_list(wb, data, log_fn):
|
|||||||
is_obj['name'] = sheet_name.strip()
|
is_obj['name'] = sheet_name.strip()
|
||||||
log_fn(f" ⚠ ИС без названия — использовано имя листа: {is_obj['name']}")
|
log_fn(f" ⚠ ИС без названия — использовано имя листа: {is_obj['name']}")
|
||||||
|
|
||||||
if is_obj['software'] and not is_obj['description']:
|
|
||||||
is_obj['description'] = f"Используемое ПО: {is_obj['software']}"
|
|
||||||
|
|
||||||
# Автоопределение категорий ПДн
|
# Автоопределение категорий ПДн
|
||||||
pd_cats = []
|
pd_cats = []
|
||||||
all_pd_text = ' '.join(p.lower() for p in pd_items_raw)
|
all_pd_text = ' '.join(p.lower() for p in pd_items_raw)
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ class InformationSystem:
|
|||||||
"""Информационная система (обработка ПДн)"""
|
"""Информационная система (обработка ПДн)"""
|
||||||
name: str = ""
|
name: str = ""
|
||||||
description: str = ""
|
description: str = ""
|
||||||
software: str = ""
|
|
||||||
is_local_network: bool = False
|
is_local_network: bool = False
|
||||||
is_internet: bool = False
|
is_internet: bool = False
|
||||||
defence_level: str = ""
|
defence_level: str = ""
|
||||||
|
|||||||
+153
-21
@@ -201,7 +201,7 @@ class DokoGenApp:
|
|||||||
|
|
||||||
addr_fields = [
|
addr_fields = [
|
||||||
("Юридический адрес:", "address"),
|
("Юридический адрес:", "address"),
|
||||||
("Город:", "city_name"),
|
("Город (заполняется автоматически):", "city_name"),
|
||||||
("ИНН:", "inn"),
|
("ИНН:", "inn"),
|
||||||
("КПП:", "kpp"),
|
("КПП:", "kpp"),
|
||||||
("ОГРН:", "ogrn"),
|
("ОГРН:", "ogrn"),
|
||||||
@@ -212,6 +212,12 @@ class DokoGenApp:
|
|||||||
entry = ttk.Entry(f2, width=50)
|
entry = ttk.Entry(f2, width=50)
|
||||||
entry.grid(row=i, column=1, padx=10, sticky=tk.EW, pady=1)
|
entry.grid(row=i, column=1, padx=10, sticky=tk.EW, pady=1)
|
||||||
self._company_entries[key] = entry
|
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)
|
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 = ttk.Entry(fields_frame, width=55)
|
||||||
self.is_description_entry.grid(row=1, column=1, padx=10, sticky=tk.EW)
|
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 = 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()
|
self.is_lan_var = tk.BooleanVar()
|
||||||
ttk.Checkbutton(chk_frame, text="ЛВС", variable=self.is_lan_var).pack(side=tk.LEFT, padx=5)
|
ttk.Checkbutton(chk_frame, text="ЛВС", variable=self.is_lan_var).pack(side=tk.LEFT, padx=5)
|
||||||
self.is_internet_var = tk.BooleanVar()
|
self.is_internet_var = tk.BooleanVar()
|
||||||
@@ -292,33 +294,42 @@ class DokoGenApp:
|
|||||||
|
|
||||||
# Тип угроз
|
# Тип угроз
|
||||||
self.is_threat_var = tk.StringVar(value="3")
|
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 = 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 (НДВ нет)")]:
|
for val, text in [("1", "1 (НДВ в СПО)"), ("2", "2 (НДВ в ППО)"), ("3", "3 (НДВ нет)")]:
|
||||||
ttk.Radiobutton(threat_frame, text=text, variable=self.is_threat_var,
|
ttk.Radiobutton(threat_frame, text=text, variable=self.is_threat_var,
|
||||||
value=val).pack(side=tk.LEFT, padx=3)
|
value=val).pack(side=tk.LEFT, padx=3)
|
||||||
|
|
||||||
# Уровень защищённости
|
# Уровень защищённости
|
||||||
self.is_defence_var = tk.StringVar(value="4")
|
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 = 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"]:
|
for lvl in ["1", "2", "3", "4"]:
|
||||||
ttk.Radiobutton(def_frame, text=f"{lvl} ур.",
|
ttk.Radiobutton(def_frame, text=f"{lvl} ур.",
|
||||||
variable=self.is_defence_var, value=lvl).pack(side=tk.LEFT, padx=3)
|
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 = 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 = []
|
self.category_vars = []
|
||||||
for cat in ["специальные", "биометрические", "общедоступные", "иные"]:
|
for cat in ["специальные", "биометрические", "общедоступные", "иные"]:
|
||||||
var = tk.BooleanVar()
|
var = tk.BooleanVar()
|
||||||
|
var.trace_add('write', lambda *_: self._on_categories_or_count_changed())
|
||||||
cb = ttk.Checkbutton(cat_frame, text=cat, variable=var)
|
cb = ttk.Checkbutton(cat_frame, text=cat, variable=var)
|
||||||
cb.pack(anchor=tk.W)
|
cb.pack(anchor=tk.W)
|
||||||
self.category_vars.append((cat, var))
|
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)
|
fields_frame.columnconfigure(1, weight=1)
|
||||||
|
|
||||||
# Вложенные списки
|
# Вложенные списки
|
||||||
@@ -405,7 +416,17 @@ class DokoGenApp:
|
|||||||
tab = ttk.Frame(self.notebook)
|
tab = ttk.Frame(self.notebook)
|
||||||
self.notebook.add(tab, text="Пользователи")
|
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)
|
frame.pack(fill=tk.BOTH, expand=True)
|
||||||
|
|
||||||
ttk.Label(frame, text="Сотрудники, имеющие доступ к ПДн",
|
ttk.Label(frame, text="Сотрудники, имеющие доступ к ПДн",
|
||||||
@@ -572,13 +593,14 @@ class DokoGenApp:
|
|||||||
isys = self.model.information_systems[self._current_is_idx]
|
isys = self.model.information_systems[self._current_is_idx]
|
||||||
isys.name = self.is_name_entry.get().strip()
|
isys.name = self.is_name_entry.get().strip()
|
||||||
isys.description = self.is_description_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_local_network = self.is_lan_var.get()
|
||||||
isys.is_internet = self.is_internet_var.get()
|
isys.is_internet = self.is_internet_var.get()
|
||||||
isys.threat_type = self.is_threat_var.get()
|
isys.threat_type = self.is_threat_var.get()
|
||||||
isys.defence_level = self.is_defence_var.get()
|
isys.defence_level = self.is_defence_var.get()
|
||||||
cats = [c for c, v in self.category_vars if v.get()]
|
cats = [c for c, v in self.category_vars if v.get()]
|
||||||
isys.personal_data_category = cats
|
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.personal_data_list = list(self.pd_listbox.get(0, tk.END))
|
||||||
isys.pd_subjects_list = list(self.pd_subjects_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_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=""):
|
def _add_commission_row(self, role="Член комиссии", position="", fio=""):
|
||||||
row = ttk.Frame(self._comm_members_frame)
|
row = ttk.Frame(self._comm_members_frame)
|
||||||
@@ -665,7 +773,7 @@ class DokoGenApp:
|
|||||||
role_var = tk.StringVar(value=role)
|
role_var = tk.StringVar(value=role)
|
||||||
role_cb = ttk.Combobox(row, textvariable=role_var, state='readonly', width=22,
|
role_cb = ttk.Combobox(row, textvariable=role_var, state='readonly', width=22,
|
||||||
values=["Председатель комиссии", "Секретарь комиссии",
|
values=["Председатель комиссии", "Секретарь комиссии",
|
||||||
"Член комиссии", "Заместитель председателя"])
|
"Член комиссии"])
|
||||||
role_cb.pack(side=tk.LEFT, padx=2)
|
role_cb.pack(side=tk.LEFT, padx=2)
|
||||||
|
|
||||||
pos_entry = ttk.Entry(row, width=30)
|
pos_entry = ttk.Entry(row, width=30)
|
||||||
@@ -702,8 +810,6 @@ class DokoGenApp:
|
|||||||
|
|
||||||
def _set_is_edit_state(self, enabled):
|
def _set_is_edit_state(self, enabled):
|
||||||
state = 'normal' if enabled else 'disabled'
|
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_lan_var.set(False)
|
||||||
self.is_internet_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_name_entry.insert(0, isys.name or "")
|
||||||
self.is_description_entry.delete(0, tk.END)
|
self.is_description_entry.delete(0, tk.END)
|
||||||
self.is_description_entry.insert(0, isys.description or "")
|
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_lan_var.set(isys.is_local_network)
|
||||||
self.is_internet_var.set(isys.is_internet)
|
self.is_internet_var.set(isys.is_internet)
|
||||||
self.is_threat_var.set(isys.threat_type or "3")
|
self.is_threat_var.set(isys.threat_type or "3")
|
||||||
self.is_defence_var.set(isys.defence_level or "4")
|
self.is_defence_var.set(isys.defence_level or "4")
|
||||||
for cat, var in self.category_vars:
|
for cat, var in self.category_vars:
|
||||||
var.set(cat in isys.personal_data_category)
|
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 [
|
for lb, items in [
|
||||||
@@ -765,7 +873,6 @@ class DokoGenApp:
|
|||||||
copy = InformationSystem(
|
copy = InformationSystem(
|
||||||
name=src.name + " (копия)",
|
name=src.name + " (копия)",
|
||||||
description=src.description,
|
description=src.description,
|
||||||
software=src.software,
|
|
||||||
is_local_network=src.is_local_network,
|
is_local_network=src.is_local_network,
|
||||||
is_internet=src.is_internet,
|
is_internet=src.is_internet,
|
||||||
defence_level=src.defence_level,
|
defence_level=src.defence_level,
|
||||||
@@ -978,6 +1085,27 @@ class DokoGenApp:
|
|||||||
|
|
||||||
os.makedirs(output_dir, exist_ok=True)
|
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():
|
def _run():
|
||||||
try:
|
try:
|
||||||
@@ -1006,12 +1134,14 @@ class DokoGenApp:
|
|||||||
self.log(f"❌ {fname}: {e}")
|
self.log(f"❌ {fname}: {e}")
|
||||||
|
|
||||||
self.log(f"✅ Готово: {len(results)} из {len(selected)}")
|
self.log(f"✅ Готово: {len(results)} из {len(selected)}")
|
||||||
|
self.root.after(0, _close_progress)
|
||||||
self.root.after(0, lambda: messagebox.showinfo(
|
self.root.after(0, lambda: messagebox.showinfo(
|
||||||
"Готово", f"Сгенерировано {len(results)} документов\n"
|
"Готово", f"Сгенерировано {len(results)} документов\n"
|
||||||
f"Папка: {output_dir}"
|
f"Папка: {output_dir}"
|
||||||
))
|
))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log(f"❌ Ошибка: {e}")
|
self.log(f"❌ Ошибка: {e}")
|
||||||
|
self.root.after(0, _close_progress)
|
||||||
self.root.after(0, lambda: messagebox.showerror("Ошибка", str(e)))
|
self.root.after(0, lambda: messagebox.showerror("Ошибка", str(e)))
|
||||||
|
|
||||||
threading.Thread(target=_run, daemon=True).start()
|
threading.Thread(target=_run, daemon=True).start()
|
||||||
@@ -1114,6 +1244,9 @@ class DokoGenApp:
|
|||||||
entry.delete(0, tk.END)
|
entry.delete(0, tk.END)
|
||||||
entry.insert(0, val)
|
entry.insert(0, val)
|
||||||
|
|
||||||
|
# Автоопределение города из адреса после импорта
|
||||||
|
self._on_address_changed()
|
||||||
|
|
||||||
# Должностные лица (маппинг)
|
# Должностные лица (маппинг)
|
||||||
officials_map = {
|
officials_map = {
|
||||||
'chief_position': 'chiefPosition',
|
'chief_position': 'chiefPosition',
|
||||||
@@ -1152,7 +1285,6 @@ class DokoGenApp:
|
|||||||
isys = InformationSystem(
|
isys = InformationSystem(
|
||||||
name=is_data.get('name', ''),
|
name=is_data.get('name', ''),
|
||||||
description=is_data.get('description', ''),
|
description=is_data.get('description', ''),
|
||||||
software=is_data.get('software', ''),
|
|
||||||
is_internet=is_data.get('isInternet', False),
|
is_internet=is_data.get('isInternet', False),
|
||||||
pd_count=is_data.get('personalDataCount', 'менее 100 000'),
|
pd_count=is_data.get('personalDataCount', 'менее 100 000'),
|
||||||
personal_data_list=is_data.get('personalDataList', []),
|
personal_data_list=is_data.get('personalDataList', []),
|
||||||
|
|||||||
@@ -169,7 +169,6 @@ def _add_is_indexed_vars(replacements: Dict[str, str], isys, idx: int):
|
|||||||
'is_users': ', '.join(isys.users_list or []) or '—',
|
'is_users': ', '.join(isys.users_list or []) or '—',
|
||||||
'is_pd_count': str(isys.pd_count or '—'),
|
'is_pd_count': str(isys.pd_count or '—'),
|
||||||
'is_purpose': isys.purpose or '—',
|
'is_purpose': isys.purpose or '—',
|
||||||
'is_software_list': isys.software or '—',
|
|
||||||
}
|
}
|
||||||
for f, v in fields.items():
|
for f, v in fields.items():
|
||||||
replacements[f'{{{{{f}_{idx}}}}}'] = str(v)
|
replacements[f'{{{{{f}_{idx}}}}}'] = str(v)
|
||||||
@@ -198,7 +197,6 @@ def _build_loop_data(company: Company, is_list: List) -> Dict[str, List[Dict]]:
|
|||||||
items.append({
|
items.append({
|
||||||
'name': isys.name or '—',
|
'name': isys.name or '—',
|
||||||
'description': isys.description or '—',
|
'description': isys.description or '—',
|
||||||
'software': isys.software or '—',
|
|
||||||
'is_has_lan': 'Да' if isys.is_local_network else 'Нет',
|
'is_has_lan': 'Да' if isys.is_local_network else 'Нет',
|
||||||
'has_internet': 'да' if isys.is_internet else 'нет',
|
'has_internet': 'да' if isys.is_internet else 'нет',
|
||||||
'defence_level': str(isys.defence_level or '—'),
|
'defence_level': str(isys.defence_level or '—'),
|
||||||
|
|||||||
Reference in New Issue
Block a user