# -*- coding: utf-8 -*- """DokoGen — Справочник переменных шаблонов и резолвер значений""" from datetime import datetime from typing import Dict, Any, List, Optional import re from . import declension as dc from .models import Company, CommissionMember def _norm_name(s): """Нормализация ФИО для сравнения: регистр, пробелы, точки, тире игнорируются.""" s = (s or '').lower().replace('ё', 'е') return re.sub(r'[\s.,;:()«»"\'\-]+', '', s) # ============================================================ # ОПИСАНИЕ ПЕРЕМЕННЫХ # ============================================================ # Формат: (имя_переменной, группа, описание, ключ_резолвера) # Группы: A — общие, B — 187-ФЗ VARIABLE_DEFS = [ # === Полное название организации (6 падежей) === ('{{company_full_name}}', 'A', 'Полное наименование организации (им.п.)', 'company:full_name'), ('{{company_full_name_genitive}}', 'A', 'Полное наименование (род.п.)', 'decl:full_name:gent'), ('{{company_full_name_dative}}', 'A', 'Полное наименование (дат.п.)', 'decl:full_name:datv'), ('{{company_full_name_accs}}', 'A', 'Полное наименование (вин.п.)', 'decl:full_name:accs'), ('{{company_full_name_ablt}}', 'A', 'Полное наименование (твор.п.)', 'decl:full_name:ablt'), ('{{company_full_name_loct}}', 'A', 'Полное наименование (пр.п.)', 'decl:full_name:loct'), # === Краткое название организации (6 падежей) === ('{{company_short_name}}', 'A', 'Краткое наименование (им.п.)', 'company:short_name'), ('{{company_short_name_genitive}}', 'A', 'Краткое наименование (род.п.)', 'decl:short_name:gent'), ('{{company_short_name_dative}}', 'A', 'Краткое наименование (дат.п.)', 'decl:short_name:datv'), ('{{company_short_name_accs}}', 'A', 'Краткое наименование (вин.п.)', 'decl:short_name:accs'), ('{{company_short_name_ablt}}', 'A', 'Краткое наименование (твор.п.)', 'decl:short_name:ablt'), ('{{company_short_name_loct}}', 'A', 'Краткое наименование (пр.п.)', 'decl:short_name:loct'), # === Должностные лица === ('{{chief_position}}', 'A', 'Должность руководителя', 'company:chief_position'), ('{{chief_fio_initials}}', 'A', 'И.О. Фамилия руководителя', 'initials:chief'), ('{{chief_fio_short}}', 'A', 'Фамилия И.О. руководителя', 'short_fio:chief'), ('{{responsible_position}}', 'A', 'Должность ответственного', 'company:responsible_position'), ('{{responsible_fio_initials}}', 'A', 'И.О. Фамилия ответственного', 'initials:responsible'), ('{{responsible_fio_short}}', 'A', 'Фамилия И.О. ответственного', 'short_fio:responsible'), ('{{admin_position}}', 'A', 'Должность администратора', 'company:admin_position'), ('{{admin_fio_initials}}', 'A', 'И.О. Фамилия администратора', 'initials:admin'), ('{{admin_fio_short}}', 'A', 'Фамилия И.О. администратора', 'short_fio:admin'), # === Сведения об организации === ('{{company_address}}', 'A', 'Адрес организации', 'company:address'), ('{{city_name}}', 'A', 'Город', 'company:city_name'), ('{{company_inn}}', 'A', 'ИНН', 'company:inn'), ('{{company_ogrn}}', 'A', 'ОГРН', 'company:ogrn'), ('{{company_ogrn_date}}', 'A', 'Дата ОГРН', 'company:ogrn_date'), ('{{company_kpp}}', 'A', 'КПП', 'company:kpp'), ('{{security_department}}', 'A', 'Наименование структурного подразделения по безопасности', 'company:security_department'), ('{{company_okved}}', 'A', 'Основной ОКВЭД', 'company:okved'), ('{{company_okved_extra}}', 'A', 'Дополнительные ОКВЭД', 'company:okved_extra'), # === Комиссия === ('{{commission_list}}', 'A', 'Список комиссии', 'commission_list'), # === Специальные переменные === ('{{date_year}}', 'A', 'Текущий год', 'date:year'), ('{{date}}', 'A', 'Текущая дата', 'date:full'), ('{{document_date}}', 'A', 'Дата документа', 'doc_date'), ('{{document_number}}', 'A', 'Номер документа', 'doc_number'), # === 187-ФЗ === ('{{is_security_requirements}}', 'B', 'Требования к защите объекта КИИ', 'security_reqs'), ('{{is_list}}', 'B', 'Список всех объектов КИИ (нумерованный)', 'is_list'), ('{{is_name}}', 'B', 'Наименование первого объекта КИИ', 'is:name'), ('{{name}}', 'B', 'Название объекта КИИ (алиас)', 'is:name'), ('{{description}}', 'B', 'Описание объекта КИИ (алиас)', 'is:description'), ('{{category}}', 'B', 'Категория значимости объекта КИИ (алиас)', 'is:category'), ('{{number}}', 'B', 'Номер (алиас)', 'doc_number'), ('{{users}}', 'B', 'Пользователи ОКИИ (алиас)', 'is:users'), ('{{full_name_genitive}}', 'A', 'Полное наименование (род.п., алиас)', 'decl:full_name:gent'), ('{{responsible_fio_accs}}', 'A', 'ФИО ответственного (вин.п.)', 'fio_acl:responsible'), ('{{commission1}}', 'A', 'Должность члена комиссии 1', 'commission_member:0:position'), ('{{commission1_fio}}', 'A', 'ФИО члена комиссии 1', 'commission_member:0:fio'), ('{{commission2}}', 'A', 'Должность члена комиссии 2', 'commission_member:1:position'), ('{{commission2_fio}}', 'A', 'ФИО члена комиссии 2', 'commission_member:1:fio'), ('{{commission3}}', 'A', 'Должность члена комиссии 3', 'commission_member:2:position'), ('{{commission3_fio}}', 'A', 'ФИО члена комиссии 3', 'commission_member:2:fio'), ] # Маппинг старых переменных <...> → {{...}} OLD_VAR_MAP = { '': '{{company_full_name}}', '': '{{company_short_name}}', '': '{{company_full_name_genitive}}', '': '{{company_short_name_genitive}}', '': '{{company_full_name_dative}}', '': '{{company_short_name_dative}}', '': '{{company_full_name_accs}}', '': '{{company_short_name_accs}}', '': '{{company_full_name_ablt}}', '': '{{company_short_name_ablt}}', '': '{{company_full_name_loct}}', '': '{{company_short_name_loct}}', '
': '{{company_address}}', '': '{{company_inn}}', '': '{{company_ogrn}}', '': '{{company_kpp}}', '': '{{chief_fio_initials}}', '': '{{chief_fio_short}}', '': '{{chief_position}}', '': '{{responsible_fio_initials}}', '': '{{responsible_fio_short}}', '': '{{responsible_position}}', '': '{{commission_list}}', '': '{{document_number}}', '': '{{document_date}}', '': '{{date}}', '': '{{document_date}}', } VAR_PATTERN = re.compile(r'\{\{[^}]+\}\}') OLD_VAR_PATTERN = re.compile(r'<[A-Za-z0-9_]+>') # ============================================================ # ПОСТРОЕНИЕ СЛОВАРЯ ЗАМЕН # ============================================================ def build_replacements( company: Company, is_list: List = None, doc_number: str = "", doc_date: str = "", direction: str = "187fz", ) -> Dict[str, str]: """Строит полный словарь замен {{переменная}} → значение.""" if is_list is None: is_list = [] # ---------- Комиссия ---------- commission_data = _build_commission_data(company) # ---------- Контекст ---------- ctx = { 'company': company, 'is_list': is_list, 'commission_data': commission_data, 'doc_number': doc_number or '', 'doc_date': doc_date or '', 'direction': direction, } replacements = {} for var_name, group, desc, source in VARIABLE_DEFS: if direction == '187fz' and group not in ('A', 'B'): continue value = _resolve(source, ctx) replacements[var_name] = str(value) if value else '—' # Индексированные переменные ИС (is_name_1, is_pd_list_2 и т.д.) for i, isys in enumerate(is_list, 1): _add_is_indexed_vars(replacements, isys, i) # Циклы replacements['_loops'] = _build_loop_data(company, is_list) return replacements def _commission_role_display(members) -> List[str]: """Группирует роли комиссии для вывода: - Председатель/Секретарь/Заместитель — отдельной строкой со своей ролью; - обычные члены — первый получает «Члены комиссии», остальные — пустую роль (чтобы не повторять «Член комиссии» в каждой строке).""" result = [] member_used = False for m in members: role = (m.role or 'член комиссии').strip() role_lower = role.lower() if ('председател' in role_lower or 'секретар' in role_lower or 'заместител' in role_lower or 'зам. председателя' in role_lower): result.append(role) continue # обычный член комиссии if not member_used: result.append('Члены комиссии') member_used = True else: result.append('') return result def _build_commission_data(company: Company) -> Dict[str, str]: data = {} members = company.commission or [] display_roles = _commission_role_display(members) lines = [] for i, (m, role_disp) in enumerate(zip(members, display_roles), 1): if role_disp: line = (f"{i}. {role_disp}: {m.position} — {m.fio}" if m.position else f"{i}. {role_disp}: {m.fio}") else: line = (f"{i}. {m.position} — {m.fio}" if m.position else f"{i}. {m.fio}") lines.append(line) data['commission_list'] = '\n'.join(lines) if lines else '—' return data def _add_is_indexed_vars(replacements: Dict[str, str], isys, idx: int): fields = { 'is_name': isys.name or '—', 'is_address': getattr(isys, 'address', '') or '—', 'is_description': isys.description or '—', 'is_has_internet': 'да' if isys.is_internet else 'нет', 'is_category': isys.category or '—', 'is_users': ', '.join(isys.users_list or []) or '—', 'is_purpose': isys.purpose or '—', 'is_structure': getattr(isys, 'structure', '') or '—', 'is_software': getattr(isys, 'software', '') or '—', 'is_sphere': getattr(isys, 'sphere', '') or '—', 'is_object_type': getattr(isys, 'object_type', '') or '—', 'is_architecture': getattr(isys, 'architecture', '') or '—', 'is_typical_object': getattr(isys, 'typical_object', '') or '—', 'is_net_category': getattr(isys, 'net_category', '') or '—', 'is_net_operator': getattr(isys, 'net_operator', '') or '—', 'is_net_purpose': getattr(isys, 'net_purpose', '') or '—', 'is_net_method': getattr(isys, 'net_method', '') or '—', 'is_hw_sw': getattr(isys, 'hw_sw', '') or '—', 'is_os_sw': getattr(isys, 'os_sw', '') or '—', 'is_app_sw': getattr(isys, 'app_sw', '') or '—', } for f, v in fields.items(): replacements[f'{{{{{f}_{idx}}}}}'] = str(v) def _build_loop_data(company: Company, is_list: List) -> Dict[str, List[Dict]]: loops = {} # Комиссия members = company.commission or [] if members: display_roles = _commission_role_display(members) items = [] for m, role_disp in zip(members, display_roles): pos = m.position or '—' fio = m.fio or '—' if role_disp: full = f"{role_disp} {pos} — {fio}" if pos != '—' else f"{role_disp} — {fio}" else: full = f"{pos} — {fio}" if pos != '—' else fio # Строка подписи: «___________ / » (только подчёркивание и слэш). # Роль НЕ включаем — в шаблонах она выводится отдельной колонкой {{item.role}}, # иначе получается дубль «Председатель комиссии | Председатель комиссии: ___». if 'Председатель' in role_disp: sig = f"{'_' * 11} / " sig_full = f"{role_disp}: {'_' * 11} / " elif role_disp: sig = f"{'_' * 17} / " sig_full = f"{role_disp}: {'_' * 17} / " else: sig = f"{'_' * 17} / " sig_full = f"{'_' * 17} / " initials = dc.get_initials(fio) if fio != '—' else '' items.append({ 'role': role_disp, 'position': pos, 'fio': fio, 'fio_initials': dc.get_initials(fio) if fio != '—' else '', 'fio_short': dc.get_short_fio(fio) if fio != '—' else '', 'full': full, # Рамка подписи без роли и ФИО: «___________ / » (ФИО добавится) 'signature': sig, # Полная подпись с ролью и ФИО: «Роль: ___________ / И.О. Фамилия» 'signature_full': (sig_full + initials).strip(), }) loops['commission'] = items # ИС if is_list: items = [] for isys in is_list: # Пользователи ИС для вложенного цикла # ВСЕ пользователи, включая администратора (без дублей по ФИО) seen = set() users_loop = [] for u in (isys.users_list or []): key = _norm_name(u) if key and key in seen: continue seen.add(key) users_loop.append({'fio': u, 'position': ''}) items.append({ 'name': isys.name or '—', 'description': isys.description or '—', 'address': getattr(isys, 'address', '') or '—', 'is_has_lan': 'Да' if isys.is_local_network else 'Нет', 'has_internet': 'да' if isys.is_internet else 'нет', 'category': isys.category or '—', 'users': ', '.join(isys.users_list or []) or '—', 'users_loop': users_loop, 'defense_tools_list': ', '.join(isys.defense_tools_list or []) or '—', 'is_purpose': isys.purpose or '—', 'structure': getattr(isys, 'structure', '') or '—', 'software': getattr(isys, 'software', '') or '—', # 236 приказ 'purpose': isys.purpose or '—', 'sphere': getattr(isys, 'sphere', '') or '—', 'object_type': getattr(isys, 'object_type', '') or '—', 'architecture': getattr(isys, 'architecture', '') or '—', 'typical_object': getattr(isys, 'typical_object', '') or '—', 'subject_name': getattr(isys, 'subject_name', '') or '—', 'subject_address': getattr(isys, 'subject_address', '') or '—', 'subject_chief_position': getattr(isys, 'subject_chief_position', '') or '—', 'subject_chief_fio': getattr(isys, 'subject_chief_fio', '') or '—', 'subject_security_position': getattr(isys, 'subject_security_position', '') or '—', 'subject_security_fio': getattr(isys, 'subject_security_fio', '') or '—', 'subject_department': getattr(isys, 'subject_department', '') or '—', 'subject_department_chief': getattr(isys, 'subject_department_chief', '') or '—', 'subject_phone': getattr(isys, 'subject_phone', '') or '—', 'subject_email': getattr(isys, 'subject_email', '') or '—', 'subject_inn': getattr(isys, 'subject_inn', '') or '—', 'subject_kpp': getattr(isys, 'subject_kpp', '') or '—', 'net_category': getattr(isys, 'net_category', '') or '—', 'net_operator': getattr(isys, 'net_operator', '') or '—', 'net_purpose': getattr(isys, 'net_purpose', '') or '—', 'net_method': getattr(isys, 'net_method', '') or '—', 'hw_sw': getattr(isys, 'hw_sw', '') or '—', 'os_sw': getattr(isys, 'os_sw', '') or '—', 'app_sw': getattr(isys, 'app_sw', '') or '—', 'attacker_category': getattr(isys, 'attacker_category', '') or '—', 'threats': getattr(isys, 'threats', '') or '—', 'incidents': getattr(isys, 'incidents', '') or '—', 'criteria_values': getattr(isys, 'criteria_values', '') or '—', 'criteria_justification': getattr(isys, 'criteria_justification', '') or '—', 'org_measures': getattr(isys, 'org_measures', '') or '—', 'tech_measures': getattr(isys, 'tech_measures', '') or '—', }) loops['information_systems'] = items # Сотрудники с доступом employees = getattr(company, 'employees_access', None) or [] if employees: items = [] for e in employees: items.append({ 'position': e.get('position', ''), 'fio': e.get('fio', ''), }) loops['employees_access'] = items return loops # ============================================================ # РЕЗОЛВЕР ЗНАЧЕНИЙ # ============================================================ def _resolve(source: str, ctx: Dict) -> str: """Преобразует ключ-источник в значение, подставляя из контекста.""" company = ctx.get('company') is_list = ctx.get('is_list', []) commission_data = ctx.get('commission_data', {}) doc_number = ctx.get('doc_number', '') doc_date = ctx.get('doc_date', '') # Прямые поля Company if source.startswith('company:'): field = source[8:] return str(getattr(company, field, '') or '') # Склонение названия if source.startswith('decl:'): _, field, case = source.split(':') name = getattr(company, field, '') or '' case_gr = dc._normalize_case(case) # Сначала берём ГОТОВОЕ склонение из модели (пользователь мог # подставить через Морфер/ИИ или ввести вручную), пересчитываем # только если поле пустое case_idx = {'nomn': 1, 'gent': 2, 'datv': 3, 'accs': 4, 'ablt': 5, 'loct': 6}.get(case_gr) if case_idx: if field == 'full_name': ready = getattr(company, f'company_name_{case_idx}', '') or '' if ready: return ready elif field == 'short_name': ready = getattr(company, f'short_name_{case_idx}', '') or '' if ready: return ready return dc.company_name_decline(name, case_gr) if name else '—' # Форматы ФИО if source.startswith('short_fio:'): who = source.split(':', 1)[1] fio = _get_fio(company, who) return dc.get_short_fio(fio) if fio else '—' if source.startswith('initials:'): who = source.split(':', 1)[1] fio = _get_fio(company, who) return dc.get_initials(fio) if fio else '—' # Комиссия if source in commission_data: return str(commission_data[source]) # Даты if source.startswith('date:'): fmt = source.split(':', 1)[1] now = datetime.now() if fmt == 'year': return str(now.year) return now.strftime('%d.%m.%Y') if source == 'doc_date': return doc_date or datetime.now().strftime('%d.%m.%Y') if source == 'doc_number': num = str(doc_number).strip() return re.sub(r'^[Nn]?[oо]?[№#]\s*', '', num) or '—' # Требования защищённости if source == 'security_reqs': return _get_security_text(ctx) # IS-алиасы (первая ИС) if source.startswith('is:'): field = source[3:] if is_list: first = is_list[0] field_map = { 'name': 'name', 'description': 'description', 'category': 'category', 'users': lambda: ', '.join(first.users_list or []), } if field in field_map: val = field_map[field] if callable(val): return val() or '—' return str(getattr(first, val, '') or '—') if isinstance(val, str) else '—' return '—' # Список ИС if source == 'is_list': if is_list: return '\n'.join(f"{i}. {isys.name}" for i, isys in enumerate(is_list, 1)) return '—' if source == 'is_list_comma': if is_list: return ', '.join(isys.name for isys in is_list) return '—' # Члены комиссии if source.startswith('commission_member:'): parts = source.split(':') idx, field = int(parts[1]), parts[2] members = getattr(company, 'commission', None) or [] if idx < len(members): return str(getattr(members[idx], field, '') or '—') return '—' # Склонение ФИО по падежам if source.startswith('fio_acl:'): who = source.split(':', 1)[1] fio = _get_fio(company, who) return dc.decline(fio, 'accs') if fio else '—' return '—' def _get_fio(company: Company, who: str) -> str: mapping = { 'chief': 'chief_fio', 'responsible': 'responsible_fio', 'admin': 'admin_fio', } field = mapping.get(who, who + '_fio') return getattr(company, field, '') or '' def _get_security_text(ctx: Dict) -> str: """Возвращает текст требований к защите объекта КИИ по категории значимости.""" is_list = ctx.get('is_list', []) category = '' if is_list: category = str(getattr(is_list[0], 'category', '') or '').strip() TEXTS = { '1': """Для значимого объекта КИИ 1-й категории значимости необходимо: — создание системы безопасности значимого объекта КИИ; — обеспечение защиты информации от компьютерных атак; — категорирование объектов КИИ и контроль его результатов.""", '2': """Для значимого объекта КИИ 2-й категории значимости необходимо: — создание системы безопасности значимого объекта КИИ; — обеспечение защиты информации от компьютерных атак.""", '3': """Для значимого объекта КИИ 3-й категории значимости необходимо: — обеспечение защиты информации от компьютерных атак.""", } if category in TEXTS: return TEXTS[category] return """Для объекта КИИ, не являющегося значимым, необходимо: — принятие мер по защите информации, предусмотренных законодательством РФ о безопасности КИИ."""