diff --git a/dokogen/generator.py b/dokogen/generator.py index 7ffcd5b..427b32d 100644 --- a/dokogen/generator.py +++ b/dokogen/generator.py @@ -282,6 +282,62 @@ def _cleanup_marker_paras(doc_xml: str) -> str: return etree.tostring(root, xml_declaration=True, encoding='UTF-8', standalone=True).decode('utf-8') +def _expand_nested_loops(template: str, item: Dict) -> str: + """Разворачивает вложенные циклы вида ... + данными из item (список словарей). Используется для циклов внутри цикла ИС, + например пользователи ИС. Переменные внутри: {{user_fio}}, {{user_position}}.""" + result = template + for _ in range(20): + m_start = LOOP_START.search(result) + if not m_start: + break + key = m_start.group(1) + data = item.get(key) + if not isinstance(data, list): + # Нет данных для вложенного цикла — удаляем блок целиком + depth = 1 + pos = m_start.end() + m_end = None + for mm in LOOP_END.finditer(result, pos): + depth -= 1 + if depth == 0: + m_end = mm + break + if m_end: + result = result[:m_start.start()] + result[m_end.end():] + continue + # Ищем соответствующий loop_end с учётом вложенности + depth = 1 + pos = m_start.end() + m_end = None + markers = sorted( + [('s', x) for x in LOOP_START.finditer(result, pos)] + + [('e', x) for x in LOOP_END.finditer(result, pos)], + key=lambda t: t[1].start() + ) + for kind, mm in markers: + if kind == 's': + depth += 1 + else: + depth -= 1 + if depth == 0: + m_end = mm + break + if m_end is None: + break + inner = result[m_start.end():m_end.start()] + parts = [] + for sub in data: + clone = inner + for f, v in sub.items(): + val = str(v) if v else '' + clone = clone.replace('{{user_%s}}' % f, val) + clone = clone.replace('{{item.%s}}' % f, val) + parts.append(clone) + result = result[:m_start.start()] + ''.join(parts) + result[m_end.end():] + return result + + def expand_loops_in_zip(docx_path: str, loops: Dict[str, List[Dict]]) -> str: """Обрабатывает циклы напрямую в ZIP/DOCX на уровне XML. Работает до загрузки python-docx — гарантирует корректную вложенность. @@ -392,7 +448,12 @@ def expand_loops_in_zip(docx_path: str, loops: Dict[str, List[Dict]]) -> str: expanded_parts = [] for idx, item in enumerate(items, 1): clone = template_clean + # Вложенные циклы (users_loop и т.п.) — разворачиваем ДО подстановки + # обычных полей, чтобы {{user_fio}} внутри них не затёрлись алиасами + clone = _expand_nested_loops(clone, item) for field, value in item.items(): + if isinstance(value, list): + continue # списки — данные вложенных циклов, не подставляем val_str = str(value) if value else ('—' if value is None else '') # С префиксом item. clone = clone.replace('{{item.%s}}' % field, val_str) diff --git a/dokogen/variables.py b/dokogen/variables.py index 28c06e2..1c967f9 100644 --- a/dokogen/variables.py +++ b/dokogen/variables.py @@ -9,6 +9,12 @@ from . import declension as dc from .models import Company, CommissionMember +def _norm_name(s): + """Нормализация ФИО для сравнения: регистр, пробелы, точки, тире игнорируются.""" + s = (s or '').lower().replace('ё', 'е') + return re.sub(r'[\s.,;:()«»"\'\-]+', '', s) + + # ============================================================ # ОПИСАНИЕ ПЕРЕМЕННЫХ # ============================================================ @@ -267,7 +273,18 @@ def _build_loop_data(company: Company, is_list: List) -> Dict[str, List[Dict]]: # ИС if is_list: items = [] + admin_fio = (getattr(company, 'admin_fio', '') or '').strip() + if not admin_fio: + admin_fio = (getattr(company, 'ispdn_fio', '') or '').strip() + admin_norm = _norm_name(admin_fio) for isys in is_list: + # Пользователи ИС для вложенного цикла + # (без администратора, чтобы он не дублировался) + users_loop = [] + for u in (isys.users_list or []): + if admin_norm and _norm_name(u) == admin_norm: + continue + users_loop.append({'fio': u, 'position': ''}) items.append({ 'name': isys.name or '—', 'description': isys.description or '—', @@ -280,6 +297,7 @@ def _build_loop_data(company: Company, is_list: List) -> Dict[str, List[Dict]]: 'pd_list': ', '.join(isys.personal_data_list or []) or '—', 'pd_count': str(isys.pd_count or '—'), 'users': ', '.join(isys.users_list or []) or '—', + 'users_loop': users_loop, '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 '—',