fix: Word разбивает маркеры циклов/переменные на run'ы + адрес ИС

- _normalize_loop_markers: склейка текста параграфа в один run перед
  поиском <!-- loop:... --> и {{item.*}}, иначе циклы не разворачивались
  (шаблон '6. Акт определения уровня защищенности ПДн.docx')
- variables.py: поле address в цикле information_systems
This commit is contained in:
2026-08-03 10:08:45 +04:00
parent 28ac7f69ab
commit ab96991a72
2 changed files with 57 additions and 0 deletions
+56
View File
@@ -68,6 +68,59 @@ OLD_VAR_MAP = {
# 2. ОБРАБОТКА ЦИКЛОВ НА УРОВНЕ ZIP/XML
# ============================================================
W_NS = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'
def _normalize_loop_markers(doc_xml: str) -> str:
"""Word разбивает маркеры <!-- loop:... --> и переменные {{item.x}}
на несколько run'ов (например '<!-- ' в одном <w:t>, 'loop:info' в другом,
'rmation -->' в третьем). Склеиваем текст каждого параграфа в один run,
чтобы маркеры и переменные циклов снова стали едиными."""
try:
root = etree.fromstring(doc_xml.encode('utf-8'))
except Exception:
return doc_xml
changed = False
for p in root.iter(f'{{{W_NS}}}p'):
# Собираем весь текст параграфа (по всем run'ам)
texts = []
for t in p.iter(f'{{{W_NS}}}t'):
texts.append(t.text or '')
full = ''.join(texts)
# Интересуют только параграфы с маркерами циклов ИЛИ переменными цикла
if 'loop:' not in full and 'loop_end' not in full and '{{item.' not in full:
continue
# Сохраняем форматирование первого run
first_r = p.find(f'{{{W_NS}}}r')
rpr = None
if first_r is not None:
rpr_el = first_r.find(f'{{{W_NS}}}rPr')
if rpr_el is not None:
rpr = etree.fromstring(etree.tostring(rpr_el))
# Оставляем только pPr, удаляем все run'ы
for child in list(p):
if child.tag != f'{{{W_NS}}}pPr':
p.remove(child)
# Создаём один run с полным текстом параграфа
new_r = etree.SubElement(p, f'{{{W_NS}}}r')
if rpr is not None:
new_r.append(rpr)
new_t = etree.SubElement(new_r, f'{{{W_NS}}}t')
new_t.text = full
new_t.set('{http://www.w3.org/XML/1998/namespace}space', 'preserve')
changed = True
if not changed:
return doc_xml
return etree.tostring(root, xml_declaration=True, encoding='UTF-8', standalone=True).decode('utf-8')
def expand_loops_in_zip(docx_path: str, loops: Dict[str, List[Dict]]) -> str:
"""Обрабатывает циклы напрямую в ZIP/DOCX на уровне XML.
Работает до загрузки python-docx — гарантирует корректную вложенность.
@@ -102,6 +155,9 @@ def expand_loops_in_zip(docx_path: str, loops: Dict[str, List[Dict]]) -> str:
if not doc_xml:
break
# Word мог разбить маркеры циклов на несколько run'ов — склеиваем
doc_xml = _normalize_loop_markers(doc_xml)
# Находим ВСЕ маркеры циклов
start_markers = list(LOOP_START.finditer(doc_xml))
end_markers = list(LOOP_END.finditer(doc_xml))
+1
View File
@@ -219,6 +219,7 @@ def _build_loop_data(company: Company, is_list: List) -> Dict[str, List[Dict]]:
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 ''),