fix: циклы через ZIP/XML до python-docx + чистка мусора
This commit is contained in:
@@ -0,0 +1,395 @@
|
||||
# -*- 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 = {
|
||||
'<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)
|
||||
|
||||
# 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'])
|
||||
Reference in New Issue
Block a user