Spaces:
Sleeping
Sleeping
Update meta.py
Browse files
meta.py
CHANGED
|
@@ -68,10 +68,6 @@ def set_two_column_layout(doc, add_separator_line=True, balance_columns=True):
|
|
| 68 |
# Set space between columns (reduced for better space utilization)
|
| 69 |
cols.set(qn('w:space'), '432') # 0.3 inch in twentieths of a point (was 708)
|
| 70 |
|
| 71 |
-
# Add separator line between columns if requested
|
| 72 |
-
if add_separator_line:
|
| 73 |
-
cols.set(qn('w:sep'), '1') # This adds the vertical separator line
|
| 74 |
-
|
| 75 |
# Enable column balancing if requested
|
| 76 |
if balance_columns:
|
| 77 |
cols.set(qn('w:equalWidth'), '1') # Equal width columns
|
|
@@ -137,27 +133,138 @@ def add_page_break(doc):
|
|
| 137 |
doc.add_page_break()
|
| 138 |
|
| 139 |
|
| 140 |
-
def create_course_title(doc, course_number, course_title, theme_color=None):
|
| 141 |
-
"""Create a course title section
|
|
|
|
|
|
|
|
|
|
| 142 |
if theme_color is None:
|
| 143 |
-
theme_color =
|
| 144 |
|
| 145 |
# Add minimal space before course title
|
| 146 |
course_para = doc.add_paragraph()
|
| 147 |
course_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
| 148 |
|
| 149 |
-
#
|
| 150 |
-
course_para.paragraph_format.space_before = Pt(
|
| 151 |
-
course_para.paragraph_format.space_after = Pt(
|
| 152 |
-
course_para.paragraph_format.keep_with_next = True
|
| 153 |
-
course_para.paragraph_format.keep_together = True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 154 |
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
course_run.font.size = Pt(13)
|
| 158 |
-
course_run.font.bold = True
|
| 159 |
-
course_run.font.color.rgb = None
|
| 160 |
-
course_run.font.color.rgb = theme_color
|
| 161 |
|
| 162 |
return course_para
|
| 163 |
|
|
@@ -186,14 +293,14 @@ def format_question_block(doc, question_num, question_text, choices, correct_ans
|
|
| 186 |
# Question number in Axiforma Black
|
| 187 |
num_run = question_para.add_run(f"{question_num}. ")
|
| 188 |
num_run.font.name = 'Inter ExtraBold'
|
| 189 |
-
num_run.font.size = Pt(
|
| 190 |
num_run.font.bold = True
|
| 191 |
num_run.font.color.rgb = theme_color
|
| 192 |
|
| 193 |
# Question text in SF UI Display Med
|
| 194 |
text_run = question_para.add_run(question_text)
|
| 195 |
text_run.font.name = 'Inter ExtraBold'
|
| 196 |
-
text_run.font.size = Pt(
|
| 197 |
|
| 198 |
# Display ALL choices for this question with minimal spacing
|
| 199 |
choice_paragraphs = []
|
|
@@ -217,7 +324,7 @@ def format_question_block(doc, question_num, question_text, choices, correct_ans
|
|
| 217 |
|
| 218 |
choice_run = choice_para.add_run(f"{choice_letter}- {choice_text}")
|
| 219 |
choice_run.font.name = 'Inter Display Medium'
|
| 220 |
-
choice_run.font.size = Pt(
|
| 221 |
|
| 222 |
choice_paragraphs.append(choice_para)
|
| 223 |
|
|
@@ -235,22 +342,22 @@ def format_question_block(doc, question_num, question_text, choices, correct_ans
|
|
| 235 |
# Source
|
| 236 |
source_run = source_para.add_run(f"Source:")
|
| 237 |
source_run.font.name = 'Inter SemiBold'
|
| 238 |
-
source_run.font.size = Pt(
|
| 239 |
source_run.font.bold = True
|
| 240 |
source_run.font.underline = True
|
| 241 |
|
| 242 |
source_value_run = source_para.add_run(f" {source}")
|
| 243 |
source_value_run.font.name = 'Inter Display Medium'
|
| 244 |
-
source_value_run.font.size = Pt(
|
| 245 |
source_value_run.font.color.rgb = None
|
| 246 |
source_value_run.font.color.rgb = theme_color
|
| 247 |
|
| 248 |
empty_para = doc.add_paragraph(' ', style='TinySpace')
|
| 249 |
empty_para.paragraph_format.space_before = Pt(0)
|
| 250 |
empty_para.paragraph_format.space_after = Pt(0)
|
| 251 |
-
empty_para.paragraph_format.line_spacing = Pt(
|
| 252 |
empty_run = empty_para.add_run(' ')
|
| 253 |
-
empty_run.font.size = Pt(
|
| 254 |
|
| 255 |
# Add comment if exists
|
| 256 |
if comment and str(comment).strip() and str(comment).lower() != 'nan':
|
|
@@ -276,7 +383,7 @@ def add_page_numbers(doc, theme_hex=None):
|
|
| 276 |
# ===== HEADER (keep existing text like module name) =====
|
| 277 |
header = section.header
|
| 278 |
header.is_linked_to_previous = False
|
| 279 |
-
section.header_distance = Cm(0.
|
| 280 |
|
| 281 |
# If header is empty, add a blank paragraph
|
| 282 |
if not header.paragraphs:
|
|
@@ -339,7 +446,7 @@ def add_page_numbers(doc, theme_hex=None):
|
|
| 339 |
run._r.append(fldChar2)
|
| 340 |
|
| 341 |
run.font.name = 'Montserrat'
|
| 342 |
-
run.font.size = Pt(
|
| 343 |
run.font.bold = True
|
| 344 |
run.font.color.rgb = RGBColor(0, 0, 0)
|
| 345 |
|
|
@@ -362,7 +469,7 @@ def add_page_numbers(doc, theme_hex=None):
|
|
| 362 |
<w:r>
|
| 363 |
<w:rPr>
|
| 364 |
<w:rFonts w:ascii="Aptos" w:hAnsi="Aptos"/>
|
| 365 |
-
<w:sz w:val="
|
| 366 |
<w:color w:val="{theme_hex}"/>
|
| 367 |
<w:u w:val="single"/>
|
| 368 |
</w:rPr>
|
|
@@ -372,7 +479,7 @@ def add_page_numbers(doc, theme_hex=None):
|
|
| 372 |
<w:rPr>
|
| 373 |
<w:rFonts w:ascii="Montserrat" w:hAnsi="Montserrat"/>
|
| 374 |
<w:b/>
|
| 375 |
-
<w:sz w:val="
|
| 376 |
<w:color w:val="{theme_hex}"/>
|
| 377 |
<w:u w:val="single"/>
|
| 378 |
</w:rPr>
|
|
@@ -980,7 +1087,7 @@ def create_flexible_header(section, module_name, sheet_name, display_name=None,
|
|
| 980 |
<w:rPr>
|
| 981 |
<w:rFonts w:ascii="Montserrat" w:hAnsi="Montserrat"/>
|
| 982 |
<w:b/>
|
| 983 |
-
<w:sz w:val="
|
| 984 |
<w:color w:val="{theme_hex}"/>
|
| 985 |
</w:rPr>
|
| 986 |
<w:t>{module_name_str}</w:t>
|
|
@@ -1011,7 +1118,7 @@ def create_flexible_header(section, module_name, sheet_name, display_name=None,
|
|
| 1011 |
<w:rPr>
|
| 1012 |
<w:rFonts w:ascii="Montserrat" w:hAnsi="Montserrat"/>
|
| 1013 |
<w:b/>
|
| 1014 |
-
<w:sz w:val="
|
| 1015 |
<w:color w:val="{theme_hex}"/>
|
| 1016 |
</w:rPr>
|
| 1017 |
<w:t>{sheet_name_str}</w:t>
|
|
@@ -1084,6 +1191,45 @@ def extract_display_name_from_excel(excel_file_path):
|
|
| 1084 |
return os.path.splitext(os.path.basename(excel_file_path))[0]
|
| 1085 |
|
| 1086 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1087 |
def process_excel_to_word(excel_file_path, output_word_path, display_name=None, use_two_columns=True, add_separator_line=True, balance_method="dynamic", theme_hex=None):
|
| 1088 |
"""Main function to process Excel and create a Word document with TOC on the first page"""
|
| 1089 |
|
|
@@ -1171,9 +1317,9 @@ def process_excel_to_word(excel_file_path, output_word_path, display_name=None,
|
|
| 1171 |
# ========================================
|
| 1172 |
# ADD THREE EMPTY PAGES AT THE BEGINNING
|
| 1173 |
# ========================================
|
| 1174 |
-
for i in range(
|
| 1175 |
doc.add_paragraph() # Add empty paragraph
|
| 1176 |
-
if i <
|
| 1177 |
doc.add_page_break()
|
| 1178 |
|
| 1179 |
# TOC helpers
|
|
@@ -1374,8 +1520,6 @@ def process_excel_to_word(excel_file_path, output_word_path, display_name=None,
|
|
| 1374 |
sectPr.append(cols)
|
| 1375 |
cols.set(qn('w:num'), '2')
|
| 1376 |
cols.set(qn('w:space'), '432')
|
| 1377 |
-
if add_separator_line:
|
| 1378 |
-
cols.set(qn('w:sep'), '1')
|
| 1379 |
cols.set(qn('w:equalWidth'), '1')
|
| 1380 |
|
| 1381 |
if use_two_columns:
|
|
@@ -1386,13 +1530,15 @@ def process_excel_to_word(excel_file_path, output_word_path, display_name=None,
|
|
| 1386 |
sectPr.append(cols)
|
| 1387 |
cols.set(qn('w:num'), '2')
|
| 1388 |
cols.set(qn('w:space'), '432')
|
| 1389 |
-
if add_separator_line:
|
| 1390 |
-
cols.set(qn('w:sep'), '1')
|
| 1391 |
cols.set(qn('w:equalWidth'), '1')
|
| 1392 |
|
| 1393 |
# Use the new flexible header function
|
| 1394 |
create_flexible_header(section, module_name, first_sheet_name, display_name, theme_hex=theme_hex)
|
| 1395 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1396 |
# ========== CUSTOMIZE MODULE TITLE APPEARANCE HERE ==========
|
| 1397 |
MODULE_HEIGHT = 31 # Frame height in points
|
| 1398 |
MODULE_ROUNDNESS = 50 # Corner roundness % (0=square, 50=pill)
|
|
@@ -1461,7 +1607,7 @@ def process_excel_to_word(excel_file_path, output_word_path, display_name=None,
|
|
| 1461 |
course_question_count = 1
|
| 1462 |
|
| 1463 |
course_title = cours_titles.get(cours_num, f"COURSE {cours_num}")
|
| 1464 |
-
course_para = create_course_title(doc, natural_num, course_title, theme_color)
|
| 1465 |
|
| 1466 |
bm_course_name = sanitize_bookmark_name(f"COURSE_{module_name}_{cours_num}")
|
| 1467 |
add_bookmark_to_paragraph(course_para, bm_course_name, bookmark_id)
|
|
|
|
| 68 |
# Set space between columns (reduced for better space utilization)
|
| 69 |
cols.set(qn('w:space'), '432') # 0.3 inch in twentieths of a point (was 708)
|
| 70 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
# Enable column balancing if requested
|
| 72 |
if balance_columns:
|
| 73 |
cols.set(qn('w:equalWidth'), '1') # Equal width columns
|
|
|
|
| 133 |
doc.add_page_break()
|
| 134 |
|
| 135 |
|
| 136 |
+
def create_course_title(doc, course_number, course_title, theme_color=None, theme_hex=None):
|
| 137 |
+
"""Create a course title section with rounded frame (unfilled) matching module style
|
| 138 |
+
Automatically wraps to two lines and doubles height if text is too long"""
|
| 139 |
+
if theme_hex is None:
|
| 140 |
+
theme_hex = THEME_COLOR_HEX
|
| 141 |
if theme_color is None:
|
| 142 |
+
theme_color = RGBColor.from_string(theme_hex)
|
| 143 |
|
| 144 |
# Add minimal space before course title
|
| 145 |
course_para = doc.add_paragraph()
|
| 146 |
course_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
| 147 |
|
| 148 |
+
# Remove all spacing before and after
|
| 149 |
+
course_para.paragraph_format.space_before = Pt(0)
|
| 150 |
+
course_para.paragraph_format.space_after = Pt(0)
|
| 151 |
+
course_para.paragraph_format.keep_with_next = True
|
| 152 |
+
course_para.paragraph_format.keep_together = True
|
| 153 |
+
|
| 154 |
+
# Format the text
|
| 155 |
+
full_text = f"{course_number}. {course_title}"
|
| 156 |
+
text_length = len(full_text)
|
| 157 |
+
|
| 158 |
+
# ========== CUSTOMIZE COURSE TITLE APPEARANCE HERE ==========
|
| 159 |
+
MAX_CHARS_SINGLE_LINE = 40 # Threshold for wrapping to two lines
|
| 160 |
+
SINGLE_LINE_HEIGHT = 31 # Frame height for single line
|
| 161 |
+
DOUBLE_LINE_HEIGHT = 55 # Frame height for two lines (almost double)
|
| 162 |
+
COURSE_ROUNDNESS = 50 # Corner roundness %
|
| 163 |
+
COURSE_FONT_SIZE = 26 # Font size in half-points (26=13pt)
|
| 164 |
+
COURSE_TEXT_COLOR = theme_hex
|
| 165 |
+
COURSE_STROKE_COLOR = theme_hex
|
| 166 |
+
COURSE_STROKE_WEIGHT = "2pt"
|
| 167 |
+
MAX_WIDTH_PT = 280 # Maximum width in points for the frame
|
| 168 |
+
# ============================================================
|
| 169 |
+
|
| 170 |
+
# Determine if we need two lines
|
| 171 |
+
needs_two_lines = text_length > MAX_CHARS_SINGLE_LINE
|
| 172 |
+
|
| 173 |
+
if needs_two_lines:
|
| 174 |
+
# Split text intelligently at a good breaking point
|
| 175 |
+
words = course_title.split()
|
| 176 |
+
mid_point = len(words) // 2
|
| 177 |
+
|
| 178 |
+
# Try to split at middle, but prefer breaking after shorter first line
|
| 179 |
+
first_line = f"{course_number}. " + " ".join(words[:mid_point])
|
| 180 |
+
second_line = " ".join(words[mid_point:])
|
| 181 |
+
|
| 182 |
+
# Adjust if first line is too long
|
| 183 |
+
while len(first_line) > MAX_CHARS_SINGLE_LINE and mid_point > 1:
|
| 184 |
+
mid_point -= 1
|
| 185 |
+
first_line = f"{course_number}. " + " ".join(words[:mid_point])
|
| 186 |
+
second_line = " ".join(words[mid_point:])
|
| 187 |
+
|
| 188 |
+
text_escaped_line1 = html.escape(first_line)
|
| 189 |
+
text_escaped_line2 = html.escape(second_line)
|
| 190 |
+
|
| 191 |
+
# Use max of both lines for width calculation
|
| 192 |
+
max_line_length = max(len(first_line), len(second_line))
|
| 193 |
+
estimated_width = min((max_line_length * 8), MAX_WIDTH_PT)
|
| 194 |
+
frame_height = DOUBLE_LINE_HEIGHT
|
| 195 |
+
|
| 196 |
+
print(f'Text: {course_title}, charachters: {text_length}')
|
| 197 |
+
print(f'split: {first_line}, {len(first_line)}, {second_line}, {len(second_line)}')
|
| 198 |
+
|
| 199 |
+
# Two-line XML
|
| 200 |
+
text_content = f'''
|
| 201 |
+
<w:r>
|
| 202 |
+
<w:rPr>
|
| 203 |
+
<w:rFonts w:ascii="Montserrat" w:hAnsi="Montserrat"/>
|
| 204 |
+
<w:b/>
|
| 205 |
+
<w:sz w:val="{COURSE_FONT_SIZE}"/>
|
| 206 |
+
<w:color w:val="{COURSE_TEXT_COLOR}"/>
|
| 207 |
+
</w:rPr>
|
| 208 |
+
<w:t>{text_escaped_line1}</w:t>
|
| 209 |
+
</w:r>
|
| 210 |
+
<w:r>
|
| 211 |
+
<w:br/>
|
| 212 |
+
</w:r>
|
| 213 |
+
<w:r>
|
| 214 |
+
<w:rPr>
|
| 215 |
+
<w:rFonts w:ascii="Montserrat" w:hAnsi="Montserrat"/>
|
| 216 |
+
<w:b/>
|
| 217 |
+
<w:sz w:val="{COURSE_FONT_SIZE}"/>
|
| 218 |
+
<w:color w:val="{COURSE_TEXT_COLOR}"/>
|
| 219 |
+
</w:rPr>
|
| 220 |
+
<w:t>{text_escaped_line2}</w:t>
|
| 221 |
+
</w:r>'''
|
| 222 |
+
else:
|
| 223 |
+
# Single line
|
| 224 |
+
estimated_width = min((text_length * 9) + 20, MAX_WIDTH_PT)
|
| 225 |
+
frame_height = SINGLE_LINE_HEIGHT
|
| 226 |
+
text_escaped = html.escape(full_text)
|
| 227 |
+
|
| 228 |
+
print(f'Text: {text_escaped}, charachters: {text_length}')
|
| 229 |
+
|
| 230 |
+
text_content = f'''
|
| 231 |
+
<w:r>
|
| 232 |
+
<w:rPr>
|
| 233 |
+
<w:rFonts w:ascii="Montserrat" w:hAnsi="Montserrat"/>
|
| 234 |
+
<w:b/>
|
| 235 |
+
<w:sz w:val="{COURSE_FONT_SIZE}"/>
|
| 236 |
+
<w:color w:val="{COURSE_TEXT_COLOR}"/>
|
| 237 |
+
</w:rPr>
|
| 238 |
+
<w:t>{text_escaped}</w:t>
|
| 239 |
+
</w:r>'''
|
| 240 |
+
|
| 241 |
+
# Create rounded rectangle shape (UNFILLED with stroke)
|
| 242 |
+
shape_xml = f'''
|
| 243 |
+
<w:r xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
|
| 244 |
+
xmlns:v="urn:schemas-microsoft-com:vml">
|
| 245 |
+
<w:pict>
|
| 246 |
+
<v:roundrect style="width:{estimated_width}pt;height:{frame_height}pt"
|
| 247 |
+
arcsize="{COURSE_ROUNDNESS}%"
|
| 248 |
+
filled="f"
|
| 249 |
+
strokecolor="#{COURSE_STROKE_COLOR}"
|
| 250 |
+
strokeweight="{COURSE_STROKE_WEIGHT}">
|
| 251 |
+
<v:textbox inset="0pt,3pt,0pt,3pt" style="v-text-anchor:middle">
|
| 252 |
+
<w:txbxContent>
|
| 253 |
+
<w:p>
|
| 254 |
+
<w:pPr>
|
| 255 |
+
<w:jc w:val="center"/>
|
| 256 |
+
<w:spacing w:before="0" w:after="0"/>
|
| 257 |
+
</w:pPr>{text_content}
|
| 258 |
+
</w:p>
|
| 259 |
+
</w:txbxContent>
|
| 260 |
+
</v:textbox>
|
| 261 |
+
</v:roundrect>
|
| 262 |
+
</w:pict>
|
| 263 |
+
</w:r>
|
| 264 |
+
'''
|
| 265 |
|
| 266 |
+
shape_element = parse_xml(shape_xml)
|
| 267 |
+
course_para._p.append(shape_element)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 268 |
|
| 269 |
return course_para
|
| 270 |
|
|
|
|
| 293 |
# Question number in Axiforma Black
|
| 294 |
num_run = question_para.add_run(f"{question_num}. ")
|
| 295 |
num_run.font.name = 'Inter ExtraBold'
|
| 296 |
+
num_run.font.size = Pt(11)
|
| 297 |
num_run.font.bold = True
|
| 298 |
num_run.font.color.rgb = theme_color
|
| 299 |
|
| 300 |
# Question text in SF UI Display Med
|
| 301 |
text_run = question_para.add_run(question_text)
|
| 302 |
text_run.font.name = 'Inter ExtraBold'
|
| 303 |
+
text_run.font.size = Pt(11)
|
| 304 |
|
| 305 |
# Display ALL choices for this question with minimal spacing
|
| 306 |
choice_paragraphs = []
|
|
|
|
| 324 |
|
| 325 |
choice_run = choice_para.add_run(f"{choice_letter}- {choice_text}")
|
| 326 |
choice_run.font.name = 'Inter Display Medium'
|
| 327 |
+
choice_run.font.size = Pt(11)
|
| 328 |
|
| 329 |
choice_paragraphs.append(choice_para)
|
| 330 |
|
|
|
|
| 342 |
# Source
|
| 343 |
source_run = source_para.add_run(f"Source:")
|
| 344 |
source_run.font.name = 'Inter SemiBold'
|
| 345 |
+
source_run.font.size = Pt(9)
|
| 346 |
source_run.font.bold = True
|
| 347 |
source_run.font.underline = True
|
| 348 |
|
| 349 |
source_value_run = source_para.add_run(f" {source}")
|
| 350 |
source_value_run.font.name = 'Inter Display Medium'
|
| 351 |
+
source_value_run.font.size = Pt(9)
|
| 352 |
source_value_run.font.color.rgb = None
|
| 353 |
source_value_run.font.color.rgb = theme_color
|
| 354 |
|
| 355 |
empty_para = doc.add_paragraph(' ', style='TinySpace')
|
| 356 |
empty_para.paragraph_format.space_before = Pt(0)
|
| 357 |
empty_para.paragraph_format.space_after = Pt(0)
|
| 358 |
+
empty_para.paragraph_format.line_spacing = Pt(7)
|
| 359 |
empty_run = empty_para.add_run(' ')
|
| 360 |
+
empty_run.font.size = Pt(7)
|
| 361 |
|
| 362 |
# Add comment if exists
|
| 363 |
if comment and str(comment).strip() and str(comment).lower() != 'nan':
|
|
|
|
| 383 |
# ===== HEADER (keep existing text like module name) =====
|
| 384 |
header = section.header
|
| 385 |
header.is_linked_to_previous = False
|
| 386 |
+
section.header_distance = Cm(0.3)
|
| 387 |
|
| 388 |
# If header is empty, add a blank paragraph
|
| 389 |
if not header.paragraphs:
|
|
|
|
| 446 |
run._r.append(fldChar2)
|
| 447 |
|
| 448 |
run.font.name = 'Montserrat'
|
| 449 |
+
run.font.size = Pt(14)
|
| 450 |
run.font.bold = True
|
| 451 |
run.font.color.rgb = RGBColor(0, 0, 0)
|
| 452 |
|
|
|
|
| 469 |
<w:r>
|
| 470 |
<w:rPr>
|
| 471 |
<w:rFonts w:ascii="Aptos" w:hAnsi="Aptos"/>
|
| 472 |
+
<w:sz w:val="44"/>
|
| 473 |
<w:color w:val="{theme_hex}"/>
|
| 474 |
<w:u w:val="single"/>
|
| 475 |
</w:rPr>
|
|
|
|
| 479 |
<w:rPr>
|
| 480 |
<w:rFonts w:ascii="Montserrat" w:hAnsi="Montserrat"/>
|
| 481 |
<w:b/>
|
| 482 |
+
<w:sz w:val="28"/>
|
| 483 |
<w:color w:val="{theme_hex}"/>
|
| 484 |
<w:u w:val="single"/>
|
| 485 |
</w:rPr>
|
|
|
|
| 1087 |
<w:rPr>
|
| 1088 |
<w:rFonts w:ascii="Montserrat" w:hAnsi="Montserrat"/>
|
| 1089 |
<w:b/>
|
| 1090 |
+
<w:sz w:val="26"/>
|
| 1091 |
<w:color w:val="{theme_hex}"/>
|
| 1092 |
</w:rPr>
|
| 1093 |
<w:t>{module_name_str}</w:t>
|
|
|
|
| 1118 |
<w:rPr>
|
| 1119 |
<w:rFonts w:ascii="Montserrat" w:hAnsi="Montserrat"/>
|
| 1120 |
<w:b/>
|
| 1121 |
+
<w:sz w:val="26"/>
|
| 1122 |
<w:color w:val="{theme_hex}"/>
|
| 1123 |
</w:rPr>
|
| 1124 |
<w:t>{sheet_name_str}</w:t>
|
|
|
|
| 1191 |
return os.path.splitext(os.path.basename(excel_file_path))[0]
|
| 1192 |
|
| 1193 |
|
| 1194 |
+
def add_colored_column_separator(section, theme_hex=None):
|
| 1195 |
+
"""Add a custom colored vertical line between columns"""
|
| 1196 |
+
if theme_hex is None:
|
| 1197 |
+
theme_hex = THEME_COLOR_HEX
|
| 1198 |
+
|
| 1199 |
+
header = section.header
|
| 1200 |
+
|
| 1201 |
+
# Find or create the first paragraph in header
|
| 1202 |
+
if not header.paragraphs:
|
| 1203 |
+
header.add_paragraph()
|
| 1204 |
+
|
| 1205 |
+
header_para = header.paragraphs[0]
|
| 1206 |
+
|
| 1207 |
+
# Create a vertical line using VML shape
|
| 1208 |
+
# The line starts AFTER the header and goes to the bottom
|
| 1209 |
+
# Adjust the "from" value to start below header (e.g., 0.8in from top)
|
| 1210 |
+
line_xml = f'''
|
| 1211 |
+
<w:r xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
|
| 1212 |
+
xmlns:v="urn:schemas-microsoft-com:vml"
|
| 1213 |
+
xmlns:o="urn:schemas-microsoft-com:office:office">
|
| 1214 |
+
<w:pict>
|
| 1215 |
+
<v:line id="columnSeparator"
|
| 1216 |
+
style="position:absolute;left:0;text-align:left;z-index:-1;
|
| 1217 |
+
mso-position-horizontal:center;
|
| 1218 |
+
mso-position-horizontal-relative:margin;
|
| 1219 |
+
mso-position-vertical-relative:page"
|
| 1220 |
+
from="0,0.49in" to="0,11.05in"
|
| 1221 |
+
strokecolor="#{theme_hex}"
|
| 1222 |
+
strokeweight="0.75pt">
|
| 1223 |
+
<o:lock v:ext="edit" aspectratio="f"/>
|
| 1224 |
+
</v:line>
|
| 1225 |
+
</w:pict>
|
| 1226 |
+
</w:r>
|
| 1227 |
+
'''
|
| 1228 |
+
|
| 1229 |
+
line_element = parse_xml(line_xml)
|
| 1230 |
+
header_para._p.append(line_element)
|
| 1231 |
+
|
| 1232 |
+
|
| 1233 |
def process_excel_to_word(excel_file_path, output_word_path, display_name=None, use_two_columns=True, add_separator_line=True, balance_method="dynamic", theme_hex=None):
|
| 1234 |
"""Main function to process Excel and create a Word document with TOC on the first page"""
|
| 1235 |
|
|
|
|
| 1317 |
# ========================================
|
| 1318 |
# ADD THREE EMPTY PAGES AT THE BEGINNING
|
| 1319 |
# ========================================
|
| 1320 |
+
for i in range(3):
|
| 1321 |
doc.add_paragraph() # Add empty paragraph
|
| 1322 |
+
if i < 2: # Add page breaks for first 2 pages (3rd page leads to TOC)
|
| 1323 |
doc.add_page_break()
|
| 1324 |
|
| 1325 |
# TOC helpers
|
|
|
|
| 1520 |
sectPr.append(cols)
|
| 1521 |
cols.set(qn('w:num'), '2')
|
| 1522 |
cols.set(qn('w:space'), '432')
|
|
|
|
|
|
|
| 1523 |
cols.set(qn('w:equalWidth'), '1')
|
| 1524 |
|
| 1525 |
if use_two_columns:
|
|
|
|
| 1530 |
sectPr.append(cols)
|
| 1531 |
cols.set(qn('w:num'), '2')
|
| 1532 |
cols.set(qn('w:space'), '432')
|
|
|
|
|
|
|
| 1533 |
cols.set(qn('w:equalWidth'), '1')
|
| 1534 |
|
| 1535 |
# Use the new flexible header function
|
| 1536 |
create_flexible_header(section, module_name, first_sheet_name, display_name, theme_hex=theme_hex)
|
| 1537 |
|
| 1538 |
+
# ADD THE COLORED SEPARATOR
|
| 1539 |
+
if add_separator_line:
|
| 1540 |
+
add_colored_column_separator(section, theme_hex)
|
| 1541 |
+
|
| 1542 |
# ========== CUSTOMIZE MODULE TITLE APPEARANCE HERE ==========
|
| 1543 |
MODULE_HEIGHT = 31 # Frame height in points
|
| 1544 |
MODULE_ROUNDNESS = 50 # Corner roundness % (0=square, 50=pill)
|
|
|
|
| 1607 |
course_question_count = 1
|
| 1608 |
|
| 1609 |
course_title = cours_titles.get(cours_num, f"COURSE {cours_num}")
|
| 1610 |
+
course_para = create_course_title(doc, natural_num, course_title, theme_color, theme_hex=theme_hex)
|
| 1611 |
|
| 1612 |
bm_course_name = sanitize_bookmark_name(f"COURSE_{module_name}_{cours_num}")
|
| 1613 |
add_bookmark_to_paragraph(course_para, bm_course_name, bookmark_id)
|