feat: вложенный цикл users_loop (пользователи ИС в таблице доступа)
- в цикле information_systems появился вложенный цикл
<!-- loop:users_loop --> ... <!-- loop_end -->
с переменными {{user_fio}}, {{user_position}}
- администратор (admin_fio/ispdn_fio) автоматически исключается
из списка пользователей — не дублируется в таблице
- сравнение ФИО нормализованное (регистр/пробелы/точки не важны)
This commit is contained in:
@@ -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')
|
return etree.tostring(root, xml_declaration=True, encoding='UTF-8', standalone=True).decode('utf-8')
|
||||||
|
|
||||||
|
|
||||||
|
def _expand_nested_loops(template: str, item: Dict) -> str:
|
||||||
|
"""Разворачивает вложенные циклы вида <!-- loop:users_loop --> ... <!-- loop_end -->
|
||||||
|
данными из 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:
|
def expand_loops_in_zip(docx_path: str, loops: Dict[str, List[Dict]]) -> str:
|
||||||
"""Обрабатывает циклы напрямую в ZIP/DOCX на уровне XML.
|
"""Обрабатывает циклы напрямую в ZIP/DOCX на уровне XML.
|
||||||
Работает до загрузки python-docx — гарантирует корректную вложенность.
|
Работает до загрузки python-docx — гарантирует корректную вложенность.
|
||||||
@@ -392,7 +448,12 @@ def expand_loops_in_zip(docx_path: str, loops: Dict[str, List[Dict]]) -> str:
|
|||||||
expanded_parts = []
|
expanded_parts = []
|
||||||
for idx, item in enumerate(items, 1):
|
for idx, item in enumerate(items, 1):
|
||||||
clone = template_clean
|
clone = template_clean
|
||||||
|
# Вложенные циклы (users_loop и т.п.) — разворачиваем ДО подстановки
|
||||||
|
# обычных полей, чтобы {{user_fio}} внутри них не затёрлись алиасами
|
||||||
|
clone = _expand_nested_loops(clone, item)
|
||||||
for field, value in item.items():
|
for field, value in item.items():
|
||||||
|
if isinstance(value, list):
|
||||||
|
continue # списки — данные вложенных циклов, не подставляем
|
||||||
val_str = str(value) if value else ('—' if value is None else '')
|
val_str = str(value) if value else ('—' if value is None else '')
|
||||||
# С префиксом item.
|
# С префиксом item.
|
||||||
clone = clone.replace('{{item.%s}}' % field, val_str)
|
clone = clone.replace('{{item.%s}}' % field, val_str)
|
||||||
|
|||||||
@@ -9,6 +9,12 @@ from . import declension as dc
|
|||||||
from .models import Company, CommissionMember
|
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:
|
if is_list:
|
||||||
items = []
|
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:
|
for isys in is_list:
|
||||||
|
# Пользователи ИС для вложенного цикла <!-- loop:item_users -->
|
||||||
|
# (без администратора, чтобы он не дублировался)
|
||||||
|
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({
|
items.append({
|
||||||
'name': isys.name or '—',
|
'name': isys.name or '—',
|
||||||
'description': isys.description 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_list': ', '.join(isys.personal_data_list or []) or '—',
|
||||||
'pd_count': str(isys.pd_count or '—'),
|
'pd_count': str(isys.pd_count or '—'),
|
||||||
'users': ', '.join(isys.users_list or []) or '—',
|
'users': ', '.join(isys.users_list or []) or '—',
|
||||||
|
'users_loop': users_loop,
|
||||||
'defense_tools_list': ', '.join(isys.defense_tools_list or []) or '—',
|
'defense_tools_list': ', '.join(isys.defense_tools_list or []) or '—',
|
||||||
'processing_modes': isys.processing_modes or '—',
|
'processing_modes': isys.processing_modes or '—',
|
||||||
'pd_subjects': '; '.join(isys.pd_subjects_list or []) or '—',
|
'pd_subjects': '; '.join(isys.pd_subjects_list or []) or '—',
|
||||||
|
|||||||
Reference in New Issue
Block a user