326 lines
14 KiB
Python
326 lines
14 KiB
Python
# -*- 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'),
|
||
]
|
||
|
||
# Маппинг старых переменных <...> → {{...}}
|
||
OLD_VAR_MAP = {
|
||
'<CompanyName1>': '{{company_full_name}}',
|
||
'<CompanyName2>': '{{company_short_name}}',
|
||
'<CompanyName1Gent>': '{{company_full_name_genitive}}',
|
||
'<CompanyName2Gent>': '{{company_short_name_genitive}}',
|
||
'<CompanyName1Datv>': '{{company_full_name_dative}}',
|
||
'<CompanyName2Datv>': '{{company_short_name_dative}}',
|
||
'<CompanyName1Accs>': '{{company_full_name_accs}}',
|
||
'<CompanyName2Accs>': '{{company_short_name_accs}}',
|
||
'<CompanyName1Ablt>': '{{company_full_name_ablt}}',
|
||
'<CompanyName2Ablt>': '{{company_short_name_ablt}}',
|
||
'<CompanyName1Loct>': '{{company_full_name_loct}}',
|
||
'<CompanyName2Loct>': '{{company_short_name_loct}}',
|
||
'<Address>': '{{company_address}}',
|
||
'<INN>': '{{company_inn}}',
|
||
'<OGRN>': '{{company_ogrn}}',
|
||
'<KPP>': '{{company_kpp}}',
|
||
'<ChiefFio>': '{{chief_fio_initials}}',
|
||
'<ChiefFioShort>': '{{chief_fio_short}}',
|
||
'<ChiefPosition>': '{{chief_position}}',
|
||
'<RspFio>': '{{responsible_fio_initials}}',
|
||
'<RspFioShort>': '{{responsible_fio_short}}',
|
||
'<RspPosition>': '{{responsible_position}}',
|
||
'<CommissionList>': '{{commission_list}}',
|
||
'<DocumentNumber>': '{{document_number}}',
|
||
'<DocumentDate>': '{{document_date}}',
|
||
'<Date>': '{{date}}',
|
||
'<SurveyDate>': '{{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 '—',
|
||
'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)
|
||
|
||
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'])
|