fix: правильная структура + main.py в корне
- Пакет dokogen/ в подпапке - main.py и requirements.txt в корне - Импорт: from dokogen.ui import main
This commit is contained in:
@@ -0,0 +1,327 @@
|
||||
# -*- 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 '—',
|
||||
'is_software_list': isys.software 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 '—',
|
||||
'software': isys.software 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'])
|
||||
Reference in New Issue
Block a user