222 lines
8.7 KiB
Python
222 lines
8.7 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Build Word template from original form file by replacing dots-lines ("..........")
|
|
với placeholders {{xxx}} theo vị trí label trong document.
|
|
|
|
Usage:
|
|
python scripts/build-word-template.py
|
|
|
|
Input: src/Backend/DYD.Api/Templates/bao-cao-template-original.docx
|
|
Output: src/Backend/DYD.Api/Templates/bao-cao-template.docx
|
|
"""
|
|
|
|
import re
|
|
import shutil
|
|
import sys
|
|
import tempfile
|
|
import xml.etree.ElementTree as ET
|
|
import zipfile
|
|
from pathlib import Path
|
|
|
|
NS_URI = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'
|
|
W = f'{{{NS_URI}}}'
|
|
ET.register_namespace('w', NS_URI)
|
|
|
|
# Paragraph index → placeholder token (after dot-line replacement).
|
|
# Determined by running script once and printing all paragraphs, then mapping manually.
|
|
# Key = paragraph index (in document iteration order)
|
|
# Value = placeholder name ({{VALUE}} will be inserted if current paragraph is dots-line
|
|
# OR appended to preceding label if current paragraph IS the label itself)
|
|
|
|
# For cover page (no dots after labels), we append placeholder AT END of label paragraph
|
|
# Format: label_text_regex → placeholder (inject at end of paragraph)
|
|
COVER_LABEL_APPEND = [
|
|
(re.compile(r'^Tên sáng kiến \(Tiếng Việt\):\s*$'), 'coverInitiativeName'),
|
|
(re.compile(r'^Tác giả/nhóm tác giả sáng kiến:\s*$'), 'coverAuthors'),
|
|
(re.compile(r'^Đơn vị công tác:\s*$'), 'coverUnit'),
|
|
(re.compile(r'^Thông tin liên hệ \(Điện thoại, Email\):\s*$'), 'coverContact'),
|
|
(re.compile(r'^NĂM 20\.\.\.$'), 'coverYear'),
|
|
]
|
|
|
|
# Map: label regex (previous paragraph) → placeholder for the dots-paragraph following
|
|
LABEL_TO_PLACEHOLDER = [
|
|
# Mẫu 01
|
|
(re.compile(r'^1\.\s*Mở đầu'), 'introduction'),
|
|
(re.compile(r'^2\.\s*Tên sáng kiến\b'), 'initiativeName'),
|
|
(re.compile(r'^3\.\s*Lĩnh vực áp dụng'), 'applicationField'),
|
|
(re.compile(r'^4\.1\s*Tình trạng giải pháp'), 'currentStatus'),
|
|
(re.compile(r'^-\s*Mục đích của sáng kiến'), 'purpose'),
|
|
(re.compile(r'^\+\s*Các bước thực hiện'), 'implementationSteps'),
|
|
(re.compile(r'^\+\s*Các điều kiện cần thiết'), 'conditions'),
|
|
(re.compile(r'^-\s*Về tính mới'), 'novelty'),
|
|
# Effectiveness 10 items
|
|
(re.compile(r'^\+\s*Tạo ra lợi ích kinh tế'), 'effEconomic'),
|
|
(re.compile(r'^\+\s*Đem lại hiệu quả trong giảng dạy'), 'effTeaching'),
|
|
(re.compile(r'^\+\s*Tăng năng suất lao động'), 'effProductivity'),
|
|
(re.compile(r'^\+\s*Nâng cao hiệu quả công việc'), 'effSocial'),
|
|
(re.compile(r'^\+\s*Nâng cao chất lượng công việc'), 'effQuality'),
|
|
(re.compile(r'^\+\s*Giảm chi phí'), 'effCost'),
|
|
(re.compile(r'^\+\s*Cải thiện môi trường'), 'effEnvironment'),
|
|
(re.compile(r'^\+\s*Bảo vệ sức khỏe'), 'effHealth'),
|
|
(re.compile(r'^\+\s*Đảm bảo an toàn lao động'), 'effLaborSafety'),
|
|
(re.compile(r'^\+\s*Nâng cao khả năng, trình độ'), 'effAwareness'),
|
|
(re.compile(r'^6\.\s*Những thông tin cần được bảo mật'), 'confidentialInfo'),
|
|
# Mẫu 02
|
|
(re.compile(r'^-\s*Chủ đầu tư tạo ra sáng kiến'), 'investorName'),
|
|
(re.compile(r'^-\s*Lĩnh vực áp dụng sáng kiến'), 'applicationField02'),
|
|
(re.compile(r'^-\s*Ngày sáng kiến được áp dụng'), 'firstApplyDate'),
|
|
(re.compile(r'^-\s*Nội dung của sáng kiến'), 'contentSummary'),
|
|
(re.compile(r'^Những thông tin cần được bảo mật'), 'confidentialInfo02'),
|
|
(re.compile(r'^Các điều kiện cần thiết để áp dụng'), 'conditions02'),
|
|
(re.compile(r'^Đánh giá lợi ích thu được hoặc dự kiến có thể thu được do áp dụng sáng kiến theo ý kiến của tác giả'), 'authorEvaluation'),
|
|
(re.compile(r'^Đánh giá lợi ích thu được hoặc dự kiến có thể thu được do áp dụng sáng kiến theo ý kiến của tổ chức'), 'trialEvaluation'),
|
|
# Mẫu 03
|
|
(re.compile(r'^1\.\s*Tên sáng kiến'), 'initiativeName03'),
|
|
(re.compile(r'^2\.\s*Tác giả chính'), 'mainAuthor03'),
|
|
(re.compile(r'^Chức vụ, đơn vị công tác'), 'position03'),
|
|
# Mẫu 04
|
|
(re.compile(r'^Kết luận'), 'conclusion'),
|
|
]
|
|
|
|
# Dots-line pattern — paragraph text (stripped, whitespace collapsed) is 50+ dots
|
|
DOTS_PATTERN = re.compile(r'^[\s\.]{50,}$')
|
|
|
|
|
|
def para_text(p):
|
|
"""Concat all w:t text of paragraph p."""
|
|
return ''.join((t.text or '') for t in p.iter(f'{W}t')).strip()
|
|
|
|
|
|
def set_para_text(p, new_text):
|
|
"""Replace paragraph's run text with single new_text. Keeps first run's properties."""
|
|
# Find all runs
|
|
runs = list(p.findall(f'{W}r'))
|
|
if not runs:
|
|
# No run — add one with text
|
|
r = ET.SubElement(p, f'{W}r')
|
|
t = ET.SubElement(r, f'{W}t')
|
|
t.text = new_text
|
|
t.set('{http://www.w3.org/XML/1998/namespace}space', 'preserve')
|
|
return
|
|
|
|
# Remove all runs except first
|
|
for r in runs[1:]:
|
|
p.remove(r)
|
|
|
|
# Clear all <w:t> in first run, leave <w:rPr> intact
|
|
first = runs[0]
|
|
for t in list(first.findall(f'{W}t')):
|
|
first.remove(t)
|
|
# Remove all non-rPr, non-text children? Leave them alone (breaks etc).
|
|
# Add new text element
|
|
t = ET.SubElement(first, f'{W}t')
|
|
t.text = new_text
|
|
t.set('{http://www.w3.org/XML/1998/namespace}space', 'preserve')
|
|
|
|
|
|
def append_placeholder_to_para(p, placeholder):
|
|
"""Append text ' {{placeholder}}' to end of paragraph (new run)."""
|
|
r = ET.SubElement(p, f'{W}r')
|
|
t = ET.SubElement(r, f'{W}t')
|
|
t.text = f' {{{{{placeholder}}}}}'
|
|
t.set('{http://www.w3.org/XML/1998/namespace}space', 'preserve')
|
|
|
|
|
|
def find_placeholder_for_label(text):
|
|
for regex, placeholder in LABEL_TO_PLACEHOLDER:
|
|
if regex.match(text):
|
|
return placeholder
|
|
return None
|
|
|
|
|
|
def find_cover_label(text):
|
|
for regex, placeholder in COVER_LABEL_APPEND:
|
|
if regex.match(text):
|
|
return placeholder
|
|
return None
|
|
|
|
|
|
def process(xml_path):
|
|
tree = ET.parse(xml_path)
|
|
root = tree.getroot()
|
|
|
|
paragraphs = list(root.iter(f'{W}p'))
|
|
|
|
prev_label_placeholder = None
|
|
used_placeholders = set()
|
|
cover_done = set() # only replace cover labels once (file has 2 cover pages)
|
|
dots_counter = 0
|
|
|
|
for i, p in enumerate(paragraphs):
|
|
text = para_text(p)
|
|
if not text:
|
|
continue
|
|
|
|
# 1. Cover label — append placeholder
|
|
cover_pl = find_cover_label(text)
|
|
if cover_pl and cover_pl not in cover_done:
|
|
append_placeholder_to_para(p, cover_pl)
|
|
cover_done.add(cover_pl)
|
|
prev_label_placeholder = None
|
|
continue
|
|
|
|
# 2. Dots line → replace with placeholder from prev label
|
|
if DOTS_PATTERN.match(text):
|
|
dots_counter += 1
|
|
if prev_label_placeholder and prev_label_placeholder not in used_placeholders:
|
|
set_para_text(p, f'{{{{{prev_label_placeholder}}}}}')
|
|
used_placeholders.add(prev_label_placeholder)
|
|
prev_label_placeholder = None
|
|
else:
|
|
# extra dots line without matching label — tag with generic counter
|
|
set_para_text(p, f'{{{{extra_{dots_counter}}}}}')
|
|
continue
|
|
|
|
# 3. Label paragraph → remember placeholder for NEXT dots line
|
|
label_pl = find_placeholder_for_label(text)
|
|
if label_pl:
|
|
prev_label_placeholder = label_pl
|
|
continue
|
|
|
|
# 4. Other paragraph — reset label if it wasn't matched
|
|
# Don't reset prev_label if current para is just description (italic note etc.)
|
|
# Keep prev_label until we see dots or a new label
|
|
|
|
tree.write(xml_path, encoding='UTF-8', xml_declaration=True)
|
|
return used_placeholders, dots_counter
|
|
|
|
|
|
def main():
|
|
repo_root = Path(__file__).parent.parent
|
|
src = repo_root / 'src/Backend/DYD.Api/Templates/bao-cao-template-original.docx'
|
|
dst = repo_root / 'src/Backend/DYD.Api/Templates/bao-cao-template.docx'
|
|
|
|
if not src.exists():
|
|
print(f'ERROR: source template not found at {src}')
|
|
sys.exit(1)
|
|
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
tmp = Path(tmpdir)
|
|
# Unzip
|
|
with zipfile.ZipFile(src, 'r') as z:
|
|
z.extractall(tmp)
|
|
doc_xml = tmp / 'word' / 'document.xml'
|
|
|
|
used, dots = process(doc_xml)
|
|
print(f'Replaced {len(used)} placeholders from {dots} dots-lines.')
|
|
print(f'Placeholders: {sorted(used)}')
|
|
|
|
# Rezip
|
|
if dst.exists():
|
|
dst.unlink()
|
|
with zipfile.ZipFile(dst, 'w', zipfile.ZIP_DEFLATED) as zout:
|
|
for path in tmp.rglob('*'):
|
|
if path.is_file():
|
|
zout.write(path, path.relative_to(tmp))
|
|
|
|
print(f'Wrote {dst}')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|