- api_settings: убраны реальные API-ключи из кода (env/файл) — 1.1 - verify: промт ИИ для проверки реквизитов больше не ссылается на 187-ФЗ как на закон о ПДн («юрист по ПДн (187-ФЗ)» → специалист по реквизитам) - ui: дата ПП РФ №127 исправлена 08.02.2019 → 08.02.2018 (3 места) - help_docs: группа B переименована «187-ФЗ (персональные данные)» → «187-ФЗ (критическая информационная инфраструктура)» - generator: убран дубль хвоста параграфа в _highlight_unmatched - importer: «магия дат» (числа 10001-79999 → дата Excel) теперь только для полей-дат (as_date), финансовые поля не ломаются - ui (_collect_is): убраны дубли пар маппинга attacker_level/attacker_types - ui (импорт): убрано наследие 152-ФЗ ispdnPosition/ispdnFio из маппинга - test_generator: переписан под модель 187-ФЗ (category/sphere/object_type вместо pd_count/threat_type/defence_level) - criteria_justifications: п.9 для здравоохранения согласован с кодом (убран «критерий неприменим» — противоречие с «применим ко всем») - criteria_applicability: Транспорт (добавлены п.1, п.2; убран п.10), Банковская сфера (добавлены п.8, п.9) — требуется экспертная сверка - ui: Бусредн вынесен в константу DEFAULT_BUDGET_AVG (данные Минфина, обновлять ежегодно); автозаполнение п.9 унифицировано с авторасчётом (ΔБ = ΔНП+ΔНДПИ+ΔДИВ+ΔВСО+ΔНС) - ui: п.8 — предупреждение при неполных данных (менее 5 лет доходов) - models: поля ndpi/div/vso/ns для п.9; комментарий income_loss уточнён - .gitignore: api_settings.json/.env не попадают в git
200 lines
6.9 KiB
Python
200 lines
6.9 KiB
Python
#!/usr/bin/env python3
|
||
"""DokoGen — test: verify generator with nested loops"""
|
||
|
||
import sys, os, json, tempfile, zipfile, shutil, re
|
||
from datetime import datetime
|
||
|
||
# Add to path
|
||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||
|
||
from dokogen.models import Company, CommissionMember, InformationSystem
|
||
from dokogen.variables import build_replacements
|
||
from dokogen.generator import process_template, expand_loops_in_zip
|
||
|
||
def create_test_template():
|
||
"""Creates a test .docx with nested loops for verification."""
|
||
from docx import Document
|
||
from docx.shared import Pt
|
||
|
||
doc = Document()
|
||
|
||
# Title
|
||
doc.add_heading('Test Document with Nested Loops', 0)
|
||
|
||
# Simple variable
|
||
doc.add_paragraph('Company: {{company_full_name}}')
|
||
doc.add_paragraph('Date: {{date}}')
|
||
doc.add_paragraph('')
|
||
|
||
# Outer loop: information_systems
|
||
doc.add_paragraph('<!-- loop:information_systems -->')
|
||
doc.add_paragraph('=== IS: {{item.name}} ===')
|
||
doc.add_paragraph('Category: {{item.category}}')
|
||
doc.add_paragraph('Sphere: {{item.sphere}}')
|
||
doc.add_paragraph('Object type: {{item.object_type}}')
|
||
doc.add_paragraph('')
|
||
|
||
# Inner loop: commission
|
||
doc.add_paragraph('Commission members:')
|
||
doc.add_paragraph('<!-- loop:commission -->')
|
||
doc.add_paragraph(' - {{item.role}}: {{item.position}} {{item.fio}}')
|
||
doc.add_paragraph('<!-- loop_end -->')
|
||
|
||
doc.add_paragraph('')
|
||
doc.add_paragraph('--- end of IS ---')
|
||
doc.add_paragraph('')
|
||
doc.add_paragraph('<!-- loop_end -->')
|
||
|
||
doc.add_paragraph('')
|
||
doc.add_paragraph('Footer: Generated automatically')
|
||
|
||
path = '/tmp/test_template_nested.docx'
|
||
doc.save(path)
|
||
print(f'✅ Test template created: {path}')
|
||
return path
|
||
|
||
def test_zip_loop_expansion():
|
||
"""Test ZIP-level loop expansion with mock data."""
|
||
print('\n' + '='*60)
|
||
print('TEST: ZIP-level loop expansion')
|
||
print('='*60)
|
||
|
||
# Create test data
|
||
company = Company(
|
||
full_name='ООО "Ромашка"',
|
||
short_name='Ромашка',
|
||
inn='7701234567',
|
||
chief_fio='Иванов Иван Иванович',
|
||
chief_position='Генеральный директор',
|
||
commission=[
|
||
CommissionMember(role='Председатель', position='Директор', fio='Иванов И.И.'),
|
||
CommissionMember(role='Секретарь', position='Секретарь', fio='Петров П.П.'),
|
||
CommissionMember(role='Член комиссии', position='Бухгалтер', fio='Сидорова А.А.'),
|
||
],
|
||
information_systems=[
|
||
InformationSystem(name='ОКИИ-1 "Платёжный шлюз"', category='1',
|
||
sphere='Банковская сфера и финансовый рынок', object_type='АСУ'),
|
||
InformationSystem(name='ОКИИ-2 "Кадры"', category='2',
|
||
sphere='Наука', object_type='ИС'),
|
||
InformationSystem(name='ОКИИ-3 "Склад"', category='3',
|
||
sphere='Здравоохранение', object_type='ИС'),
|
||
]
|
||
)
|
||
|
||
# Build replacements
|
||
replacements = build_replacements(
|
||
company=company,
|
||
is_list=company.information_systems,
|
||
doc_number='001',
|
||
doc_date='31.07.2026',
|
||
direction='187fz'
|
||
)
|
||
|
||
loops = replacements.get('_loops', {})
|
||
print(f'\nLoops found: {list(loops.keys())}')
|
||
print(f'IS items: {len(loops.get("information_systems", []))}')
|
||
print(f'Commission items: {len(loops.get("commission", []))}')
|
||
|
||
# Create test template
|
||
template_path = create_test_template()
|
||
|
||
# Verify template has markers
|
||
with zipfile.ZipFile(template_path, 'r') as z:
|
||
doc_xml = z.read('word/document.xml').decode('utf-8')
|
||
|
||
start_count = doc_xml.count('loop:')
|
||
end_count = doc_xml.count('loop_end')
|
||
print(f'\nMarkers in template: {start_count} starts, {end_count} ends')
|
||
|
||
# Process with ZIP-level loop expansion
|
||
print('\nProcessing loops via ZIP...')
|
||
result_path = expand_loops_in_zip(template_path, loops)
|
||
|
||
# Verify result
|
||
with zipfile.ZipFile(result_path, 'r') as z:
|
||
result_xml = z.read('word/document.xml').decode('utf-8')
|
||
|
||
remaining_starts = len(re.findall(r'loop:(\w+)', result_xml))
|
||
remaining_ends = result_xml.count('loop_end')
|
||
|
||
print(f'Markers after expansion: {remaining_starts} starts, {remaining_ends} ends')
|
||
|
||
if remaining_starts == 0 and remaining_ends == 0:
|
||
print('✅ ALL LOOPS EXPANDED SUCCESSFULLY')
|
||
else:
|
||
print(f'⚠️ {remaining_starts} loop markers remaining')
|
||
|
||
# Now test full process_template
|
||
print('\n' + '='*60)
|
||
print('TEST: Full process_template with all replacements')
|
||
print('='*60)
|
||
|
||
# Rebuild replacements (they were consumed)
|
||
replacements2 = build_replacements(
|
||
company=company,
|
||
is_list=company.information_systems,
|
||
doc_number='001',
|
||
doc_date='31.07.2026',
|
||
direction='187fz'
|
||
)
|
||
|
||
try:
|
||
output_path = process_template(
|
||
template_path=template_path,
|
||
replacements=replacements2,
|
||
output_path='/tmp/test_output.docx',
|
||
log_func=print
|
||
)
|
||
print(f'\n✅ Output generated: {output_path}')
|
||
|
||
# Verify output
|
||
with zipfile.ZipFile(output_path, 'r') as z:
|
||
out_xml = z.read('word/document.xml').decode('utf-8')
|
||
|
||
# Check no remaining variables
|
||
remaining_vars = re.findall(r'\{\{[^}]+\}\}', out_xml)
|
||
remaining_old = re.findall(r'<[A-Za-z_]+>', out_xml)
|
||
|
||
if remaining_vars:
|
||
print(f'⚠️ Remaining {{...}} vars: {remaining_vars[:5]}')
|
||
else:
|
||
print('✅ No remaining {{...}} variables')
|
||
|
||
if remaining_old:
|
||
print(f'⚠️ Remaining <...> vars: {remaining_old[:5]}')
|
||
else:
|
||
print('✅ No remaining <...> variables')
|
||
|
||
# Check IS names appear
|
||
for isys in company.information_systems:
|
||
if isys.name in out_xml:
|
||
print(f'✅ Found IS name: {isys.name}')
|
||
else:
|
||
print(f'⚠️ MISSING IS name: {isys.name}')
|
||
|
||
# Check commission members appear
|
||
for m in company.commission:
|
||
if m.fio in out_xml:
|
||
print(f'✅ Found commission: {m.fio}')
|
||
else:
|
||
print(f'⚠️ MISSING commission: {m.fio}')
|
||
|
||
except Exception as e:
|
||
print(f'❌ Error: {e}')
|
||
import traceback
|
||
traceback.print_exc()
|
||
|
||
# Cleanup
|
||
os.unlink(template_path)
|
||
if os.path.exists(result_path):
|
||
os.unlink(result_path)
|
||
if os.path.exists('/tmp/test_output.docx'):
|
||
os.unlink('/tmp/test_output.docx')
|
||
|
||
print('\n' + '='*60)
|
||
print('TEST COMPLETE')
|
||
print('='*60)
|
||
|
||
if __name__ == '__main__':
|
||
test_zip_loop_expansion()
|