Spaces:
Sleeping
Sleeping
Update prof.py
Browse files
prof.py
CHANGED
|
@@ -93,10 +93,6 @@ def set_two_column_layout(doc, add_separator_line=True, balance_columns=True):
|
|
| 93 |
# Set space between columns (reduced for better space utilization)
|
| 94 |
cols.set(qn('w:space'), '432') # 0.3 inch in twentieths of a point (was 708)
|
| 95 |
|
| 96 |
-
# Add separator line between columns if requested
|
| 97 |
-
if add_separator_line:
|
| 98 |
-
cols.set(qn('w:sep'), '1') # This adds the vertical separator line
|
| 99 |
-
|
| 100 |
# Enable column balancing if requested
|
| 101 |
if balance_columns:
|
| 102 |
cols.set(qn('w:equalWidth'), '1') # Equal width columns
|
|
@@ -162,27 +158,138 @@ def add_page_break(doc):
|
|
| 162 |
doc.add_page_break()
|
| 163 |
|
| 164 |
|
| 165 |
-
def create_course_title(doc, course_number, course_title, theme_color=None):
|
| 166 |
-
"""Create a course title section
|
|
|
|
|
|
|
|
|
|
| 167 |
if theme_color is None:
|
| 168 |
-
theme_color =
|
| 169 |
|
| 170 |
# Add minimal space before course title
|
| 171 |
course_para = doc.add_paragraph()
|
| 172 |
course_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
| 173 |
|
| 174 |
-
#
|
| 175 |
-
course_para.paragraph_format.space_before = Pt(
|
| 176 |
-
course_para.paragraph_format.space_after = Pt(
|
| 177 |
-
course_para.paragraph_format.keep_with_next = True
|
| 178 |
-
course_para.paragraph_format.keep_together = True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 179 |
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 186 |
|
| 187 |
return course_para
|
| 188 |
|
|
@@ -211,14 +318,14 @@ def format_question_block(doc, question_num, question_text, choices, correct_ans
|
|
| 211 |
# Question number in Axiforma Black
|
| 212 |
num_run = question_para.add_run(f"{question_num}. ")
|
| 213 |
num_run.font.name = 'Inter ExtraBold'
|
| 214 |
-
num_run.font.size = Pt(
|
| 215 |
num_run.font.bold = True
|
| 216 |
num_run.font.color.rgb = theme_color
|
| 217 |
|
| 218 |
# Question text in SF UI Display Med
|
| 219 |
text_run = question_para.add_run(question_text)
|
| 220 |
text_run.font.name = 'Inter ExtraBold'
|
| 221 |
-
text_run.font.size = Pt(
|
| 222 |
|
| 223 |
# Display ALL choices for this question with minimal spacing
|
| 224 |
choice_paragraphs = []
|
|
@@ -242,7 +349,7 @@ def format_question_block(doc, question_num, question_text, choices, correct_ans
|
|
| 242 |
|
| 243 |
choice_run = choice_para.add_run(f"{choice_letter}- {choice_text}")
|
| 244 |
choice_run.font.name = 'Inter Display Medium'
|
| 245 |
-
choice_run.font.size = Pt(
|
| 246 |
|
| 247 |
choice_paragraphs.append(choice_para)
|
| 248 |
|
|
@@ -298,13 +405,13 @@ def format_question_block(doc, question_num, question_text, choices, correct_ans
|
|
| 298 |
|
| 299 |
answer_run = left_para.add_run("Réponse:")
|
| 300 |
answer_run.font.name = 'Inter SemiBold'
|
| 301 |
-
answer_run.font.size = Pt(
|
| 302 |
answer_run.font.bold = True
|
| 303 |
answer_run.font.underline = True
|
| 304 |
|
| 305 |
answer_run = left_para.add_run(f' {correct_answers}')
|
| 306 |
-
answer_run.font.name = 'Inter
|
| 307 |
-
answer_run.font.size = Pt(
|
| 308 |
answer_run.font.color.rgb = theme_color
|
| 309 |
|
| 310 |
# RIGHT cell - Source
|
|
@@ -315,13 +422,13 @@ def format_question_block(doc, question_num, question_text, choices, correct_ans
|
|
| 315 |
|
| 316 |
source_run = right_para.add_run("Source:")
|
| 317 |
source_run.font.name = 'Inter SemiBold'
|
| 318 |
-
source_run.font.size = Pt(
|
| 319 |
source_run.font.bold = True
|
| 320 |
source_run.font.underline = True
|
| 321 |
|
| 322 |
source_value_run = right_para.add_run(f" {source}")
|
| 323 |
-
source_value_run.font.name = 'Inter
|
| 324 |
-
source_value_run.font.size = Pt(
|
| 325 |
source_value_run.font.color.rgb = theme_color
|
| 326 |
|
| 327 |
# Keep with comment if exists
|
|
@@ -331,9 +438,9 @@ def format_question_block(doc, question_num, question_text, choices, correct_ans
|
|
| 331 |
empty_para = doc.add_paragraph(' ', style='TinySpace')
|
| 332 |
empty_para.paragraph_format.space_before = Pt(0)
|
| 333 |
empty_para.paragraph_format.space_after = Pt(0)
|
| 334 |
-
empty_para.paragraph_format.line_spacing = Pt(
|
| 335 |
empty_run = empty_para.add_run(' ')
|
| 336 |
-
empty_run.font.size = Pt(
|
| 337 |
|
| 338 |
# Add comment if exists
|
| 339 |
if comment and str(comment).strip() and str(comment).lower() != 'nan':
|
|
@@ -359,7 +466,7 @@ def add_page_numbers(doc, theme_hex=None):
|
|
| 359 |
# ===== HEADER (keep existing text like module name) =====
|
| 360 |
header = section.header
|
| 361 |
header.is_linked_to_previous = False
|
| 362 |
-
section.header_distance = Cm(0.
|
| 363 |
|
| 364 |
# If header is empty, add a blank paragraph
|
| 365 |
if not header.paragraphs:
|
|
@@ -422,7 +529,7 @@ def add_page_numbers(doc, theme_hex=None):
|
|
| 422 |
run._r.append(fldChar2)
|
| 423 |
|
| 424 |
run.font.name = 'Montserrat'
|
| 425 |
-
run.font.size = Pt(
|
| 426 |
run.font.bold = True
|
| 427 |
run.font.color.rgb = RGBColor(0, 0, 0)
|
| 428 |
|
|
@@ -445,7 +552,7 @@ def add_page_numbers(doc, theme_hex=None):
|
|
| 445 |
<w:r>
|
| 446 |
<w:rPr>
|
| 447 |
<w:rFonts w:ascii="Aptos" w:hAnsi="Aptos"/>
|
| 448 |
-
<w:sz w:val="
|
| 449 |
<w:color w:val="{theme_hex}"/>
|
| 450 |
<w:u w:val="single"/>
|
| 451 |
</w:rPr>
|
|
@@ -455,7 +562,7 @@ def add_page_numbers(doc, theme_hex=None):
|
|
| 455 |
<w:rPr>
|
| 456 |
<w:rFonts w:ascii="Montserrat" w:hAnsi="Montserrat"/>
|
| 457 |
<w:b/>
|
| 458 |
-
<w:sz w:val="
|
| 459 |
<w:color w:val="{theme_hex}"/>
|
| 460 |
<w:u w:val="single"/>
|
| 461 |
</w:rPr>
|
|
@@ -778,7 +885,7 @@ def create_flexible_header(section, module_name, sheet_name, display_name=None,
|
|
| 778 |
<w:rPr>
|
| 779 |
<w:rFonts w:ascii="Montserrat" w:hAnsi="Montserrat"/>
|
| 780 |
<w:b/>
|
| 781 |
-
<w:sz w:val="
|
| 782 |
<w:color w:val="{theme_hex}"/>
|
| 783 |
</w:rPr>
|
| 784 |
<w:t>{module_name_str}</w:t>
|
|
@@ -809,7 +916,7 @@ def create_flexible_header(section, module_name, sheet_name, display_name=None,
|
|
| 809 |
<w:rPr>
|
| 810 |
<w:rFonts w:ascii="Montserrat" w:hAnsi="Montserrat"/>
|
| 811 |
<w:b/>
|
| 812 |
-
<w:sz w:val="
|
| 813 |
<w:color w:val="{theme_hex}"/>
|
| 814 |
</w:rPr>
|
| 815 |
<w:t>{sheet_name_str}</w:t>
|
|
@@ -831,6 +938,45 @@ def create_flexible_header(section, module_name, sheet_name, display_name=None,
|
|
| 831 |
header_para._p.append(right_textbox_element)
|
| 832 |
|
| 833 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 834 |
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):
|
| 835 |
"""Main function to process Excel and create a Word document with TOC on the first page"""
|
| 836 |
|
|
@@ -918,9 +1064,9 @@ def process_excel_to_word(excel_file_path, output_word_path, display_name=None,
|
|
| 918 |
# ========================================
|
| 919 |
# ADD THREE EMPTY PAGES AT THE BEGINNING
|
| 920 |
# ========================================
|
| 921 |
-
for i in range(
|
| 922 |
doc.add_paragraph() # Add empty paragraph
|
| 923 |
-
if i <
|
| 924 |
doc.add_page_break()
|
| 925 |
|
| 926 |
# TOC helpers
|
|
@@ -1121,8 +1267,6 @@ def process_excel_to_word(excel_file_path, output_word_path, display_name=None,
|
|
| 1121 |
sectPr.append(cols)
|
| 1122 |
cols.set(qn('w:num'), '2')
|
| 1123 |
cols.set(qn('w:space'), '432')
|
| 1124 |
-
if add_separator_line:
|
| 1125 |
-
cols.set(qn('w:sep'), '1')
|
| 1126 |
cols.set(qn('w:equalWidth'), '1')
|
| 1127 |
|
| 1128 |
if use_two_columns:
|
|
@@ -1133,13 +1277,15 @@ def process_excel_to_word(excel_file_path, output_word_path, display_name=None,
|
|
| 1133 |
sectPr.append(cols)
|
| 1134 |
cols.set(qn('w:num'), '2')
|
| 1135 |
cols.set(qn('w:space'), '432')
|
| 1136 |
-
if add_separator_line:
|
| 1137 |
-
cols.set(qn('w:sep'), '1')
|
| 1138 |
cols.set(qn('w:equalWidth'), '1')
|
| 1139 |
|
| 1140 |
# Use the new flexible header function
|
| 1141 |
create_flexible_header(section, module_name, first_sheet_name, display_name, theme_hex=theme_hex)
|
| 1142 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1143 |
# ========== CUSTOMIZE MODULE TITLE APPEARANCE HERE ==========
|
| 1144 |
MODULE_HEIGHT = 31 # Frame height in points
|
| 1145 |
MODULE_ROUNDNESS = 50 # Corner roundness % (0=square, 50=pill)
|
|
@@ -1208,7 +1354,7 @@ def process_excel_to_word(excel_file_path, output_word_path, display_name=None,
|
|
| 1208 |
course_question_count = 1
|
| 1209 |
|
| 1210 |
course_title = cours_titles.get(cours_num, f"COURSE {cours_num}")
|
| 1211 |
-
course_para = create_course_title(doc, natural_num, course_title, theme_color)
|
| 1212 |
|
| 1213 |
bm_course_name = sanitize_bookmark_name(f"COURSE_{module_name}_{cours_num}")
|
| 1214 |
add_bookmark_to_paragraph(course_para, bm_course_name, bookmark_id)
|
|
|
|
| 93 |
# Set space between columns (reduced for better space utilization)
|
| 94 |
cols.set(qn('w:space'), '432') # 0.3 inch in twentieths of a point (was 708)
|
| 95 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
# Enable column balancing if requested
|
| 97 |
if balance_columns:
|
| 98 |
cols.set(qn('w:equalWidth'), '1') # Equal width columns
|
|
|
|
| 158 |
doc.add_page_break()
|
| 159 |
|
| 160 |
|
| 161 |
+
def create_course_title(doc, course_number, course_title, theme_color=None, theme_hex=None):
|
| 162 |
+
"""Create a course title section with rounded frame (unfilled) matching module style
|
| 163 |
+
Automatically wraps to two lines and doubles height if text is too long"""
|
| 164 |
+
if theme_hex is None:
|
| 165 |
+
theme_hex = THEME_COLOR_HEX
|
| 166 |
if theme_color is None:
|
| 167 |
+
theme_color = RGBColor.from_string(theme_hex)
|
| 168 |
|
| 169 |
# Add minimal space before course title
|
| 170 |
course_para = doc.add_paragraph()
|
| 171 |
course_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
| 172 |
|
| 173 |
+
# Remove all spacing before and after
|
| 174 |
+
course_para.paragraph_format.space_before = Pt(0)
|
| 175 |
+
course_para.paragraph_format.space_after = Pt(0)
|
| 176 |
+
course_para.paragraph_format.keep_with_next = True
|
| 177 |
+
course_para.paragraph_format.keep_together = True
|
| 178 |
+
|
| 179 |
+
# Format the text
|
| 180 |
+
full_text = f"{course_number}. {course_title}"
|
| 181 |
+
text_length = len(full_text)
|
| 182 |
+
|
| 183 |
+
# ========== CUSTOMIZE COURSE TITLE APPEARANCE HERE ==========
|
| 184 |
+
MAX_CHARS_SINGLE_LINE = 40 # Threshold for wrapping to two lines
|
| 185 |
+
SINGLE_LINE_HEIGHT = 31 # Frame height for single line
|
| 186 |
+
DOUBLE_LINE_HEIGHT = 55 # Frame height for two lines (almost double)
|
| 187 |
+
COURSE_ROUNDNESS = 50 # Corner roundness %
|
| 188 |
+
COURSE_FONT_SIZE = 26 # Font size in half-points (26=13pt)
|
| 189 |
+
COURSE_TEXT_COLOR = theme_hex
|
| 190 |
+
COURSE_STROKE_COLOR = theme_hex
|
| 191 |
+
COURSE_STROKE_WEIGHT = "2pt"
|
| 192 |
+
MAX_WIDTH_PT = 280 # Maximum width in points for the frame
|
| 193 |
+
# ============================================================
|
| 194 |
+
|
| 195 |
+
# Determine if we need two lines
|
| 196 |
+
needs_two_lines = text_length > MAX_CHARS_SINGLE_LINE
|
| 197 |
+
|
| 198 |
+
if needs_two_lines:
|
| 199 |
+
# Split text intelligently at a good breaking point
|
| 200 |
+
words = course_title.split()
|
| 201 |
+
mid_point = len(words) // 2
|
| 202 |
+
|
| 203 |
+
# Try to split at middle, but prefer breaking after shorter first line
|
| 204 |
+
first_line = f"{course_number}. " + " ".join(words[:mid_point])
|
| 205 |
+
second_line = " ".join(words[mid_point:])
|
| 206 |
+
|
| 207 |
+
# Adjust if first line is too long
|
| 208 |
+
while len(first_line) > MAX_CHARS_SINGLE_LINE and mid_point > 1:
|
| 209 |
+
mid_point -= 1
|
| 210 |
+
first_line = f"{course_number}. " + " ".join(words[:mid_point])
|
| 211 |
+
second_line = " ".join(words[mid_point:])
|
| 212 |
+
|
| 213 |
+
text_escaped_line1 = html.escape(first_line)
|
| 214 |
+
text_escaped_line2 = html.escape(second_line)
|
| 215 |
+
|
| 216 |
+
# Use max of both lines for width calculation
|
| 217 |
+
max_line_length = max(len(first_line), len(second_line))
|
| 218 |
+
estimated_width = min((max_line_length * 8), MAX_WIDTH_PT)
|
| 219 |
+
frame_height = DOUBLE_LINE_HEIGHT
|
| 220 |
+
|
| 221 |
+
print(f'Text: {course_title}, charachters: {text_length}')
|
| 222 |
+
print(f'split: {first_line}, {len(first_line)}, {second_line}, {len(second_line)}')
|
| 223 |
+
|
| 224 |
+
# Two-line XML
|
| 225 |
+
text_content = f'''
|
| 226 |
+
<w:r>
|
| 227 |
+
<w:rPr>
|
| 228 |
+
<w:rFonts w:ascii="Montserrat" w:hAnsi="Montserrat"/>
|
| 229 |
+
<w:b/>
|
| 230 |
+
<w:sz w:val="{COURSE_FONT_SIZE}"/>
|
| 231 |
+
<w:color w:val="{COURSE_TEXT_COLOR}"/>
|
| 232 |
+
</w:rPr>
|
| 233 |
+
<w:t>{text_escaped_line1}</w:t>
|
| 234 |
+
</w:r>
|
| 235 |
+
<w:r>
|
| 236 |
+
<w:br/>
|
| 237 |
+
</w:r>
|
| 238 |
+
<w:r>
|
| 239 |
+
<w:rPr>
|
| 240 |
+
<w:rFonts w:ascii="Montserrat" w:hAnsi="Montserrat"/>
|
| 241 |
+
<w:b/>
|
| 242 |
+
<w:sz w:val="{COURSE_FONT_SIZE}"/>
|
| 243 |
+
<w:color w:val="{COURSE_TEXT_COLOR}"/>
|
| 244 |
+
</w:rPr>
|
| 245 |
+
<w:t>{text_escaped_line2}</w:t>
|
| 246 |
+
</w:r>'''
|
| 247 |
+
else:
|
| 248 |
+
# Single line
|
| 249 |
+
estimated_width = min((text_length * 9) + 20, MAX_WIDTH_PT)
|
| 250 |
+
frame_height = SINGLE_LINE_HEIGHT
|
| 251 |
+
text_escaped = html.escape(full_text)
|
| 252 |
+
|
| 253 |
+
print(f'Text: {text_escaped}, charachters: {text_length}')
|
| 254 |
|
| 255 |
+
text_content = f'''
|
| 256 |
+
<w:r>
|
| 257 |
+
<w:rPr>
|
| 258 |
+
<w:rFonts w:ascii="Montserrat" w:hAnsi="Montserrat"/>
|
| 259 |
+
<w:b/>
|
| 260 |
+
<w:sz w:val="{COURSE_FONT_SIZE}"/>
|
| 261 |
+
<w:color w:val="{COURSE_TEXT_COLOR}"/>
|
| 262 |
+
</w:rPr>
|
| 263 |
+
<w:t>{text_escaped}</w:t>
|
| 264 |
+
</w:r>'''
|
| 265 |
+
|
| 266 |
+
# Create rounded rectangle shape (UNFILLED with stroke)
|
| 267 |
+
shape_xml = f'''
|
| 268 |
+
<w:r xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
|
| 269 |
+
xmlns:v="urn:schemas-microsoft-com:vml">
|
| 270 |
+
<w:pict>
|
| 271 |
+
<v:roundrect style="width:{estimated_width}pt;height:{frame_height}pt"
|
| 272 |
+
arcsize="{COURSE_ROUNDNESS}%"
|
| 273 |
+
filled="f"
|
| 274 |
+
strokecolor="#{COURSE_STROKE_COLOR}"
|
| 275 |
+
strokeweight="{COURSE_STROKE_WEIGHT}">
|
| 276 |
+
<v:textbox inset="0pt,3pt,0pt,3pt" style="v-text-anchor:middle">
|
| 277 |
+
<w:txbxContent>
|
| 278 |
+
<w:p>
|
| 279 |
+
<w:pPr>
|
| 280 |
+
<w:jc w:val="center"/>
|
| 281 |
+
<w:spacing w:before="0" w:after="0"/>
|
| 282 |
+
</w:pPr>{text_content}
|
| 283 |
+
</w:p>
|
| 284 |
+
</w:txbxContent>
|
| 285 |
+
</v:textbox>
|
| 286 |
+
</v:roundrect>
|
| 287 |
+
</w:pict>
|
| 288 |
+
</w:r>
|
| 289 |
+
'''
|
| 290 |
+
|
| 291 |
+
shape_element = parse_xml(shape_xml)
|
| 292 |
+
course_para._p.append(shape_element)
|
| 293 |
|
| 294 |
return course_para
|
| 295 |
|
|
|
|
| 318 |
# Question number in Axiforma Black
|
| 319 |
num_run = question_para.add_run(f"{question_num}. ")
|
| 320 |
num_run.font.name = 'Inter ExtraBold'
|
| 321 |
+
num_run.font.size = Pt(11)
|
| 322 |
num_run.font.bold = True
|
| 323 |
num_run.font.color.rgb = theme_color
|
| 324 |
|
| 325 |
# Question text in SF UI Display Med
|
| 326 |
text_run = question_para.add_run(question_text)
|
| 327 |
text_run.font.name = 'Inter ExtraBold'
|
| 328 |
+
text_run.font.size = Pt(11)
|
| 329 |
|
| 330 |
# Display ALL choices for this question with minimal spacing
|
| 331 |
choice_paragraphs = []
|
|
|
|
| 349 |
|
| 350 |
choice_run = choice_para.add_run(f"{choice_letter}- {choice_text}")
|
| 351 |
choice_run.font.name = 'Inter Display Medium'
|
| 352 |
+
choice_run.font.size = Pt(11)
|
| 353 |
|
| 354 |
choice_paragraphs.append(choice_para)
|
| 355 |
|
|
|
|
| 405 |
|
| 406 |
answer_run = left_para.add_run("Réponse:")
|
| 407 |
answer_run.font.name = 'Inter SemiBold'
|
| 408 |
+
answer_run.font.size = Pt(9)
|
| 409 |
answer_run.font.bold = True
|
| 410 |
answer_run.font.underline = True
|
| 411 |
|
| 412 |
answer_run = left_para.add_run(f' {correct_answers}')
|
| 413 |
+
answer_run.font.name = 'Inter SemiBold'
|
| 414 |
+
answer_run.font.size = Pt(9)
|
| 415 |
answer_run.font.color.rgb = theme_color
|
| 416 |
|
| 417 |
# RIGHT cell - Source
|
|
|
|
| 422 |
|
| 423 |
source_run = right_para.add_run("Source:")
|
| 424 |
source_run.font.name = 'Inter SemiBold'
|
| 425 |
+
source_run.font.size = Pt(9)
|
| 426 |
source_run.font.bold = True
|
| 427 |
source_run.font.underline = True
|
| 428 |
|
| 429 |
source_value_run = right_para.add_run(f" {source}")
|
| 430 |
+
source_value_run.font.name = 'Inter SemiBold'
|
| 431 |
+
source_value_run.font.size = Pt(9)
|
| 432 |
source_value_run.font.color.rgb = theme_color
|
| 433 |
|
| 434 |
# Keep with comment if exists
|
|
|
|
| 438 |
empty_para = doc.add_paragraph(' ', style='TinySpace')
|
| 439 |
empty_para.paragraph_format.space_before = Pt(0)
|
| 440 |
empty_para.paragraph_format.space_after = Pt(0)
|
| 441 |
+
empty_para.paragraph_format.line_spacing = Pt(7)
|
| 442 |
empty_run = empty_para.add_run(' ')
|
| 443 |
+
empty_run.font.size = Pt(7)
|
| 444 |
|
| 445 |
# Add comment if exists
|
| 446 |
if comment and str(comment).strip() and str(comment).lower() != 'nan':
|
|
|
|
| 466 |
# ===== HEADER (keep existing text like module name) =====
|
| 467 |
header = section.header
|
| 468 |
header.is_linked_to_previous = False
|
| 469 |
+
section.header_distance = Cm(0.3)
|
| 470 |
|
| 471 |
# If header is empty, add a blank paragraph
|
| 472 |
if not header.paragraphs:
|
|
|
|
| 529 |
run._r.append(fldChar2)
|
| 530 |
|
| 531 |
run.font.name = 'Montserrat'
|
| 532 |
+
run.font.size = Pt(14)
|
| 533 |
run.font.bold = True
|
| 534 |
run.font.color.rgb = RGBColor(0, 0, 0)
|
| 535 |
|
|
|
|
| 552 |
<w:r>
|
| 553 |
<w:rPr>
|
| 554 |
<w:rFonts w:ascii="Aptos" w:hAnsi="Aptos"/>
|
| 555 |
+
<w:sz w:val="44"/>
|
| 556 |
<w:color w:val="{theme_hex}"/>
|
| 557 |
<w:u w:val="single"/>
|
| 558 |
</w:rPr>
|
|
|
|
| 562 |
<w:rPr>
|
| 563 |
<w:rFonts w:ascii="Montserrat" w:hAnsi="Montserrat"/>
|
| 564 |
<w:b/>
|
| 565 |
+
<w:sz w:val="28"/>
|
| 566 |
<w:color w:val="{theme_hex}"/>
|
| 567 |
<w:u w:val="single"/>
|
| 568 |
</w:rPr>
|
|
|
|
| 885 |
<w:rPr>
|
| 886 |
<w:rFonts w:ascii="Montserrat" w:hAnsi="Montserrat"/>
|
| 887 |
<w:b/>
|
| 888 |
+
<w:sz w:val="26"/>
|
| 889 |
<w:color w:val="{theme_hex}"/>
|
| 890 |
</w:rPr>
|
| 891 |
<w:t>{module_name_str}</w:t>
|
|
|
|
| 916 |
<w:rPr>
|
| 917 |
<w:rFonts w:ascii="Montserrat" w:hAnsi="Montserrat"/>
|
| 918 |
<w:b/>
|
| 919 |
+
<w:sz w:val="26"/>
|
| 920 |
<w:color w:val="{theme_hex}"/>
|
| 921 |
</w:rPr>
|
| 922 |
<w:t>{sheet_name_str}</w:t>
|
|
|
|
| 938 |
header_para._p.append(right_textbox_element)
|
| 939 |
|
| 940 |
|
| 941 |
+
def add_colored_column_separator(section, theme_hex=None):
|
| 942 |
+
"""Add a custom colored vertical line between columns"""
|
| 943 |
+
if theme_hex is None:
|
| 944 |
+
theme_hex = THEME_COLOR_HEX
|
| 945 |
+
|
| 946 |
+
header = section.header
|
| 947 |
+
|
| 948 |
+
# Find or create the first paragraph in header
|
| 949 |
+
if not header.paragraphs:
|
| 950 |
+
header.add_paragraph()
|
| 951 |
+
|
| 952 |
+
header_para = header.paragraphs[0]
|
| 953 |
+
|
| 954 |
+
# Create a vertical line using VML shape
|
| 955 |
+
# The line starts AFTER the header and goes to the bottom
|
| 956 |
+
# Adjust the "from" value to start below header (e.g., 0.8in from top)
|
| 957 |
+
line_xml = f'''
|
| 958 |
+
<w:r xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
|
| 959 |
+
xmlns:v="urn:schemas-microsoft-com:vml"
|
| 960 |
+
xmlns:o="urn:schemas-microsoft-com:office:office">
|
| 961 |
+
<w:pict>
|
| 962 |
+
<v:line id="columnSeparator"
|
| 963 |
+
style="position:absolute;left:0;text-align:left;z-index:-1;
|
| 964 |
+
mso-position-horizontal:center;
|
| 965 |
+
mso-position-horizontal-relative:margin;
|
| 966 |
+
mso-position-vertical-relative:page"
|
| 967 |
+
from="0,0.49in" to="0,11.05in"
|
| 968 |
+
strokecolor="#{theme_hex}"
|
| 969 |
+
strokeweight="0.75pt">
|
| 970 |
+
<o:lock v:ext="edit" aspectratio="f"/>
|
| 971 |
+
</v:line>
|
| 972 |
+
</w:pict>
|
| 973 |
+
</w:r>
|
| 974 |
+
'''
|
| 975 |
+
|
| 976 |
+
line_element = parse_xml(line_xml)
|
| 977 |
+
header_para._p.append(line_element)
|
| 978 |
+
|
| 979 |
+
|
| 980 |
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):
|
| 981 |
"""Main function to process Excel and create a Word document with TOC on the first page"""
|
| 982 |
|
|
|
|
| 1064 |
# ========================================
|
| 1065 |
# ADD THREE EMPTY PAGES AT THE BEGINNING
|
| 1066 |
# ========================================
|
| 1067 |
+
for i in range(3):
|
| 1068 |
doc.add_paragraph() # Add empty paragraph
|
| 1069 |
+
if i < 2: # Add page breaks for first 2 pages (3rd page leads to TOC)
|
| 1070 |
doc.add_page_break()
|
| 1071 |
|
| 1072 |
# TOC helpers
|
|
|
|
| 1267 |
sectPr.append(cols)
|
| 1268 |
cols.set(qn('w:num'), '2')
|
| 1269 |
cols.set(qn('w:space'), '432')
|
|
|
|
|
|
|
| 1270 |
cols.set(qn('w:equalWidth'), '1')
|
| 1271 |
|
| 1272 |
if use_two_columns:
|
|
|
|
| 1277 |
sectPr.append(cols)
|
| 1278 |
cols.set(qn('w:num'), '2')
|
| 1279 |
cols.set(qn('w:space'), '432')
|
|
|
|
|
|
|
| 1280 |
cols.set(qn('w:equalWidth'), '1')
|
| 1281 |
|
| 1282 |
# Use the new flexible header function
|
| 1283 |
create_flexible_header(section, module_name, first_sheet_name, display_name, theme_hex=theme_hex)
|
| 1284 |
|
| 1285 |
+
# ADD THE COLORED SEPARATOR
|
| 1286 |
+
if add_separator_line:
|
| 1287 |
+
add_colored_column_separator(section, theme_hex)
|
| 1288 |
+
|
| 1289 |
# ========== CUSTOMIZE MODULE TITLE APPEARANCE HERE ==========
|
| 1290 |
MODULE_HEIGHT = 31 # Frame height in points
|
| 1291 |
MODULE_ROUNDNESS = 50 # Corner roundness % (0=square, 50=pill)
|
|
|
|
| 1354 |
course_question_count = 1
|
| 1355 |
|
| 1356 |
course_title = cours_titles.get(cours_num, f"COURSE {cours_num}")
|
| 1357 |
+
course_para = create_course_title(doc, natural_num, course_title, theme_color, theme_hex=theme_hex)
|
| 1358 |
|
| 1359 |
bm_course_name = sanitize_bookmark_name(f"COURSE_{module_name}_{cours_num}")
|
| 1360 |
add_bookmark_to_paragraph(course_para, bm_course_name, bookmark_id)
|