# -*- 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 # ============================================================ # ОПИСАНИЕ ПЕРЕМЕННЫХ # ============================================================ # Формат: (имя_переменной, группа, описание, ключ_резолвера) # Группы: A — общие, B — 152-ФЗ 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'), # === Комиссия === ('{{commission_list}}', 'A', 'Список комиссии', 'commission_list'), # === Специальные переменные === ('{{date_year}}', 'A', 'Текущий год', 'date:year'), ('{{date}}', 'A', 'Текущая дата', 'date:full'), ('{{document_date}}', 'A', 'Дата документа', 'doc_date'), ('{{document_number}}', 'A', 'Номер документа', 'doc_number'), # === 152-ФЗ === ('{{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'), ('{{pd_count}}', 'B', 'Количество записей ПДн (алиас)', 'is:pd_count'), ('{{pd_list}}', 'B', 'Список ПДн (алиас)', 'is:pd_list'), ('{{pd_subjects}}', 'B', 'Субъекты ПДн (алиас)', 'is:pd_subjects'), ('{{threat_type}}', 'B', 'Тип угроз (алиас)', 'is:threat_type'), ('{{users}}', 'B', 'Пользователи ИС (алиас)', 'is:users'), ('{{defence_level}}', 'B', 'Уровень защищённости (алиас)', 'is:defence_level'), ('{{full_name_genitive}}', 'A', 'Полное наименование (род.п., алиас)', 'decl:full_name:gent'), ('{{document}}', 'A', 'Название бумажного документа (алиас)', 'company:paper_documents'), ('{{storage}}', 'A', 'Место хранения (алиас)', 'company:paper_storage'), ('{{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 = "152fz", ) -> 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 == '152fz' 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 _build_commission_data(company: Company) -> Dict[str, str]: data = {} members = company.commission or [] lines = [] for i, m in enumerate(members, 1): role = m.role or 'член комиссии' line = f"{i}. {role}: {m.position} — {m.fio}" if m.position else f"{i}. {role}: {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_defence_level': str(isys.defence_level or '—'), 'is_category': isys.category or '—', 'is_pd_list': ', '.join(isys.personal_data_list or []) or '—', 'is_users': ', '.join(isys.users_list or []) or '—', 'is_pd_count': str(isys.pd_count or '—'), 'is_purpose': isys.purpose 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: items = [] for m in members: items.append({ 'role': m.role or 'член комиссии', 'position': m.position or '—', 'fio': m.fio or '—', 'full': f"{m.role} {m.position} — {m.fio}" if m.position else f"{m.role} — {m.fio}", }) loops['commission'] = items # ИС if is_list: items = [] for isys in is_list: 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 'нет', 'defence_level': str(isys.defence_level or '—'), 'threat_type': str(isys.threat_type or '3'), 'category': isys.category or '—', 'pd_list': ', '.join(isys.personal_data_list or []) or '—', 'pd_count': str(isys.pd_count or '—'), 'users': ', '.join(isys.users_list or []) or '—', 'defense_tools_list': '; '.join(isys.defense_tools_list or []) or '—', 'processing_modes': isys.processing_modes or '—', 'pd_subjects': '; '.join(isys.pd_subjects_list or []) or '—', 'is_purpose': isys.purpose or '—', }) loops['information_systems'] = items # Бумажные документы paper_list = getattr(company, 'paper_documents_list', None) or [] if paper_list: items = [] for p in paper_list: items.append({ 'document': p.name or '', 'storage': p.storage or '', }) loops['paper_documents'] = 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 '' return dc.company_name_decline(name, case) 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', 'defence_level': 'defence_level', 'threat_type': 'threat_type', 'pd_list': lambda: ', '.join(first.personal_data_list or []), 'pd_count': 'pd_count', 'pd_subjects': lambda: '; '.join(first.pd_subjects_list or []), '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: """Возвращает текст требований для уровня защищённости.""" defence_level = ctx.get('extended_fields', {}).get('defence_level', '4') is_list = ctx.get('is_list', []) if not defence_level and is_list: defence_level = str(getattr(is_list[0], 'defence_level', '4') or '4') dl = str(defence_level).strip()[0] TEXTS = { '1': """Для обеспечения 1-го уровня защищённости...""", '2': """Для обеспечения 2-го уровня защищённости...""", '3': """Для обеспечения 3-го уровня защищённости...""", '4': """Для обеспечения 4-го уровня защищённости персональных данных необходимо: — режим безопасности помещений; — сохранность носителей ПДн; — утверждение перечня лиц с доступом к ПДн; — использование сертифицированных СЗИ.""", } return TEXTS.get(dl, TEXTS['4'])