- api_settings: убраны реальные API-ключи из кода (чтение из env/файла) — 1.1
- variables: {{is_security_requirements}} больше не подставляет всегда УЗ-4;
extended_fields/defence_level передаются в контекст — 1.2
- defence.py: эталонная таблица ПП-1119 (п.9-12), 48 комбинаций;
ui.py: авторасчёт УЗ переведён на неё (public больше не 'всегда 4',
спец/био/общедоступные/иные разделены) — 2.1
- generator: убран дубль хвоста параграфа в _highlight_unmatched — 2.2
- importer: категории ПДн взаимоисключающие ('иные' только если нет
спец/био); УЗ пересчитывается при импорте — 2.3
- import_settings(.py/.json): убран перехват 'администратора безопасности'
правилом ответственного за ПДн — 3.1
- variables: {{category}}/{{is.category}} резолвятся из personal_data_category — 3.2
- verify: ИИ-предложения ИНН/ОГРН/КПП валидируются контрольными суммами
перед применением — 3.3
- models: defence_level None вместо '' (не маскирует 'не рассчитано')
- .gitignore: api_settings.json/.env не попадают в git
- test_defence.py: юнит-тест всех 48 комбинаций таблицы УЗ
175 lines
10 KiB
Python
175 lines
10 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""DokoGen — настройки импорта из опросника (редактируемый словарь).
|
||
|
||
Идея: «что импортировать и откуда» хранится НЕ в коде, а в текстовом
|
||
JSON-файле рядом с программой (import_settings.json). При сборке .exe
|
||
файл остаётся редактируемым — можно править пути к ячейкам и ключевые
|
||
слова без пересборки.
|
||
|
||
Формат файла:
|
||
{
|
||
"common_sheet_keyword": "общие", // ключевое слово листа «Общие сведения»
|
||
"employees_sheet_keyword": "сотруд", // ключевое слово листа «Сотрудники»
|
||
"is_sheet_exclude_keywords": ["общие сведения", "сотрудники"], // листы, которые НЕ ИС
|
||
"value_column": 1, // колонка значения (0=A, 1=B, ...)
|
||
"fields": [
|
||
{
|
||
"name": "Полное наименование организации", // человекочитаемое название
|
||
"var": "fullName", // ключ в данных программы
|
||
"cell": "'Общие сведения'!B2", // (необязательно) конкретная ячейка
|
||
"keywords": "полное наименование организации" // (необязательно) регэксп поиска по колонке A
|
||
}
|
||
]
|
||
}
|
||
|
||
Порядок применения для каждого поля:
|
||
1) если задана cell и ячейка заполнена — берём значение оттуда;
|
||
2) иначе ищем строку, где колонка A совпадает с keywords (регэксп),
|
||
и берём значение из колонки value_column.
|
||
"""
|
||
|
||
import os
|
||
import re
|
||
import sys
|
||
import json
|
||
|
||
# ============================================================
|
||
# ПУТЬ К ФАЙЛУ НАСТРОЕК ПО УМОЛЧАНИЮ
|
||
# ============================================================
|
||
def default_import_settings_path():
|
||
"""Путь к файлу настроек: рядом с exe (frozen) или рядом с модулем."""
|
||
if getattr(sys, 'frozen', False):
|
||
base = os.path.dirname(sys.executable)
|
||
else:
|
||
base = os.path.dirname(os.path.abspath(__file__))
|
||
return os.path.join(base, 'import_settings.json')
|
||
|
||
|
||
# ============================================================
|
||
# ДЕФОЛТНЫЙ СЛОВАРЬ (используется, если файл не найден)
|
||
# ============================================================
|
||
def _f(name, var, cell='', keywords=''):
|
||
return {'name': name, 'var': var, 'cell': cell, 'keywords': keywords}
|
||
|
||
|
||
DEFAULT_SETTINGS = {
|
||
'common_sheet_keyword': 'общие',
|
||
'employees_sheet_keyword': 'сотруд',
|
||
'is_sheet_exclude_keywords': ['общие сведения', 'сотрудники'],
|
||
'value_column': 1,
|
||
'fields': [
|
||
_f('Полное наименование организации', 'fullName', keywords=r'полное наименование организации'),
|
||
_f('Сокращённое наименование организации', 'shortName', keywords=r'сокращенное наименование организации'),
|
||
_f('Юридический адрес', 'addressLegal', keywords=r'адрес организации \(юридический\)'),
|
||
_f('Фактический адрес', 'addressActual', keywords=r'адрес организации \(фактический\)'),
|
||
_f('ИНН', 'inn', keywords=r'инн'),
|
||
_f('Дата ОГРН', 'ogrnDate', keywords=r'дата.*огрн'),
|
||
_f('ОГРН', 'ogrn', keywords=r'огрн'),
|
||
_f('КПП', 'kpp', keywords=r'кпп'),
|
||
_f('Основной ОКВЭД', 'okved', keywords=r'основной оквэд'),
|
||
_f('Должность руководителя', 'chiefPosition', keywords=r'должность руководителя'),
|
||
_f('ФИО руководителя', 'chiefFio', keywords=r'фио руководителя'),
|
||
_f('Должность администратора безопасности', 'administratorPosition', keywords=r'должность.*администрат.*(?:безопас|защит|пд)'),
|
||
_f('ФИО администратора безопасности', 'administratorFio', keywords=r'фио.*администрат.*(?:безопас|защит|пд)'),
|
||
_f('Должность ответственного за ПДн', 'ispdnPosition', keywords=r'должность.*защит.*информ|должность.*ответственн.*пдн'),
|
||
_f('ФИО ответственного за ПДн', 'ispdnFio', keywords=r'фио.*защит.*информ|фио.*ответственн.*пдн'),
|
||
_f('Email ответственного за ПДн', 'ispdnEmail', keywords=r'электронная почта.*защит|электронная почта.*ответств|электронная почта.*специалист'),
|
||
_f('Телефон ответственного за ПДн', 'ispdnPhone', keywords=r'телефон.*защит|телефон.*ответств|телефон.*специалист'),
|
||
_f('Структурное подразделение по безопасности', 'ispdnDepartment', keywords=r'структур.*подраздел'),
|
||
_f('Номер договора', 'contractNumber', keywords=r'номер.*договора|номер.*контракта'),
|
||
_f('Дата договора', 'contractDate', keywords=r'дата.*договора|дата.*контракта'),
|
||
_f('Перечень бумажных документов', 'paperDocuments', keywords=r'названи.*документ'),
|
||
_f('Место хранения документов', 'paperStorage', keywords=r'место.*хранен'),
|
||
_f('Должность ответственного за обработку ПДн', 'responsiblePosition', keywords=r'должность.*ответственн'),
|
||
_f('ФИО ответственного за обработку ПДн', 'responsibleFio', keywords=r'фио.*ответственн'),
|
||
_f('Сайт организации', 'site_name', keywords=r'сайт организации'),
|
||
_f('Должность ответственного за сайт', 'site_responsible_position', keywords=r'должность ответственного за сайт|должность.*отв.*сайт'),
|
||
_f('ФИО ответственного за сайт', 'site_responsible_fio', keywords=r'фио.*отв.*сайт|фио.*сайт'),
|
||
_f('Разработчик сайта', 'site_service_provider', keywords=r'наименование организации.*разрабат.*сайт|наименование.*размещала.*сайт|разрабат.*сайт.*организац'),
|
||
_f('ИНН разработчика сайта', 'site_service_provider_inn', keywords=r'инн.*разрабат.*сайт|инн.*размещала.*сайт'),
|
||
_f('Размещение сайта (хостинг)', 'site_hosting', keywords=r'размещение сайта|адрес расположения серверов|адрес.*сервер.*сайт'),
|
||
_f('Адрес хостинга', 'site_hosting_address', keywords=r'адрес.*хостинг|адрес.*располож.*сервер'),
|
||
_f('Электронная почта', 'email', keywords=r'электронная почта'),
|
||
_f('Телефон', 'phone', keywords=r'телефон'),
|
||
],
|
||
}
|
||
|
||
|
||
# ============================================================
|
||
# ЗАГРУЗКА НАСТРОЕК
|
||
# ============================================================
|
||
def load_settings(path=None):
|
||
"""Загрузка словаря импорта из JSON-файла.
|
||
|
||
path=None -> файл по умолчанию рядом с программой.
|
||
Если файла нет или он битый — возвращается встроенный дефолт.
|
||
"""
|
||
if path is None:
|
||
path = default_import_settings_path()
|
||
|
||
if not path or not os.path.exists(path):
|
||
return DEFAULT_SETTINGS
|
||
|
||
try:
|
||
with open(path, encoding='utf-8-sig') as f:
|
||
loaded = json.load(f)
|
||
# Дополняем недостающие ключи из дефолта
|
||
merged = dict(DEFAULT_SETTINGS)
|
||
for k, v in loaded.items():
|
||
if k == 'fields' and isinstance(v, list):
|
||
merged['fields'] = v
|
||
else:
|
||
merged[k] = v
|
||
return merged
|
||
except Exception:
|
||
return DEFAULT_SETTINGS
|
||
|
||
|
||
def ensure_default_file(path=None):
|
||
"""Создаёт файл настроек import_settings.json, если его ещё нет.
|
||
|
||
Возвращает путь к файлу. Нужно, чтобы пользователь видел словарь
|
||
импорта и мог править его в блокноте без пересборки программы.
|
||
"""
|
||
if path is None:
|
||
path = default_import_settings_path()
|
||
if os.path.exists(path):
|
||
return path
|
||
try:
|
||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||
with open(path, 'w', encoding='utf-8') as f:
|
||
json.dump(DEFAULT_SETTINGS, f, ensure_ascii=False, indent=2)
|
||
except Exception:
|
||
pass
|
||
return path
|
||
|
||
|
||
def parse_cell_ref(cell_ref):
|
||
"""Разбор ссылки на ячейку: 'Общие сведения'!B2 / Общие сведения!B2 / B2.
|
||
|
||
Возвращает (sheet_name или None, координаты 'B2').
|
||
"""
|
||
if not cell_ref:
|
||
return None, ''
|
||
ref = cell_ref.strip()
|
||
if ref.startswith('='):
|
||
ref = ref[1:].strip()
|
||
if '!' in ref:
|
||
sheet_part, coord = ref.split('!', 1)
|
||
sheet_part = sheet_part.strip()
|
||
if sheet_part.startswith("'") and sheet_part.endswith("'"):
|
||
sheet_part = sheet_part[1:-1]
|
||
return sheet_part or None, coord.strip().upper()
|
||
return None, ref.upper()
|
||
|
||
|
||
def cell_value(ws, coord):
|
||
"""Значение ячейки по координате 'B2' (с нормализацией)."""
|
||
if not coord:
|
||
return ''
|
||
try:
|
||
val = ws[coord]
|
||
return val.value if val is not None else ''
|
||
except Exception:
|
||
return ''
|