File size: 30,353 Bytes
6cf0508
e85cb9c
662cf94
e28d5a2
662cf94
 
 
e28d5a2
662cf94
 
9f7873d
6cf0508
662cf94
e28d5a2
6cf0508
e85cb9c
 
 
6cf0508
e28d5a2
6cf0508
 
 
 
 
662cf94
e28d5a2
6cf0508
662cf94
 
 
6cf0508
 
 
 
 
 
 
 
 
 
 
 
 
 
662cf94
 
 
 
 
 
 
6cf0508
e85cb9c
 
e28d5a2
 
6cf0508
e28d5a2
6cf0508
 
e85cb9c
6cf0508
e28d5a2
 
 
 
 
 
9f7873d
6cf0508
9f7873d
6cf0508
 
e85cb9c
e28d5a2
6cf0508
 
 
 
 
 
 
 
 
 
 
9f7873d
6cf0508
9f7873d
 
 
 
 
6cf0508
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e85cb9c
 
 
 
6cf0508
 
 
 
 
e28d5a2
662cf94
6cf0508
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
662cf94
6cf0508
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e28d5a2
662cf94
6cf0508
 
 
 
 
 
 
 
e28d5a2
662cf94
6cf0508
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e28d5a2
662cf94
6cf0508
 
 
 
 
 
 
 
 
 
 
 
e28d5a2
662cf94
6cf0508
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e28d5a2
662cf94
6cf0508
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e28d5a2
 
 
 
 
 
 
6cf0508
662cf94
6cf0508
e28d5a2
 
 
662cf94
6cf0508
e28d5a2
6cf0508
 
 
 
 
 
 
 
 
 
 
e85cb9c
 
6cf0508
 
 
662cf94
e28d5a2
662cf94
e28d5a2
e85cb9c
662cf94
e85cb9c
662cf94
 
6cf0508
 
e28d5a2
6cf0508
e28d5a2
 
 
 
 
6cf0508
e85cb9c
 
e28d5a2
 
6cf0508
 
 
e28d5a2
 
 
 
 
 
6cf0508
e28d5a2
6cf0508
 
 
e28d5a2
6cf0508
 
 
e28d5a2
6cf0508
 
 
 
e28d5a2
 
6cf0508
 
 
e85cb9c
 
 
6cf0508
 
 
 
 
e28d5a2
 
 
e85cb9c
e28d5a2
6cf0508
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e28d5a2
 
6cf0508
 
 
 
e28d5a2
 
e85cb9c
e28d5a2
 
 
6cf0508
e85cb9c
6cf0508
e85cb9c
 
6cf0508
 
 
 
 
 
 
 
 
 
 
 
 
e85cb9c
e28d5a2
 
662cf94
 
 
e28d5a2
 
6cf0508
 
 
 
 
e28d5a2
6cf0508
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
# --- START OF COMPLETE FLASK APP SCRIPT (v5) ---
from flask import Flask, render_template, request, send_file, flash, redirect, url_for
import os
import convertapi # For PDF conversion
from docx import Document
from docx.shared import Pt, Cm, RGBColor
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.enum.table import WD_TABLE_ALIGNMENT, WD_ALIGN_VERTICAL, WD_ROW_HEIGHT_RULE
from docx.oxml.ns import nsdecls
from docx.oxml import parse_xml
import math
import traceback # For detailed error logging

# --- Configuration ---
# Use the provided ConvertAPI secret
convertapi.api_secret = 'secret_8wCI6pgOP9AxLVJG'

# Define a temporary directory for generated files
UPLOAD_FOLDER = 'temp_files'
if not os.path.exists(UPLOAD_FOLDER):
    try:
        os.makedirs(UPLOAD_FOLDER)
    except OSError as e:
        print(f"Error creating directory {UPLOAD_FOLDER}: {e}")
        # Handle the error appropriately, maybe exit or use a default path


# --- Classe de génération de document (v5 - Finalized for Flask) ---
class EvaluationGymnique:
    def __init__(self):
        self.document = Document()
        # --- Document Setup (Margins, Page Size) ---
        try:
            section = self.document.sections[0]
            section.page_height = Cm(29.7)
            section.page_width = Cm(21)
            section.left_margin = Cm(1.5)
            section.right_margin = Cm(1.5)
            section.top_margin = Cm(1)
            section.bottom_margin = Cm(1)
        except Exception as e:
            print(f"Error setting up document sections: {e}")


        # --- Default Properties ---
        self.centre_examen = "Centre d'examen"
        self.type_examen = "Bac Général"
        self.serie = "Série"
        self.etablissement = "Établissement"
        self.session = "2025"
        self.nom_candidat = "Candidat"
        self.elements_techniques = []
        self.appreciations = ["M", "PM", "NM", "NR"] # Not directly used in output, but good to have

        # --- Layout Parameters ---
        self.base_font_size = 10
        self.base_header_font_size = 14
        self.base_row_height = 1.1 # Adjusted base height
        self.table_font_size = 9
        self.available_height = 27.7 # A4 height minus margins
        self.fixed_elements_height = 15 # Estimated height of non-main-table elements

        # --- Dynamic Sizes (Initialized) ---
        self.dynamic_font_size = self.base_font_size
        self.dynamic_header_font_size = self.base_header_font_size
        self.dynamic_table_font_size = self.table_font_size
        self.dynamic_row_height = self.base_row_height
        self.spacing_factor = 1.0

    def calculate_dynamic_sizing(self):
        """Adjusts font sizes and spacing based on the number of elements."""
        num_elements = len(self.elements_techniques)
        # Estimate table height considering element name + 1 newline
        estimated_table_height = (num_elements + 1) * self.base_row_height * 1.3 # Moderate multiplier

        available_space_for_table = self.available_height - self.fixed_elements_height

        if estimated_table_height > available_space_for_table and num_elements > 5: # Start adjusting after 5 elements
            # More elements -> smaller sizes
            reduction_factor = max(0.6, 1 - (max(0, num_elements - 5) * 0.04)) # Gradual reduction
            self.dynamic_font_size = max(self.base_font_size * reduction_factor, 7) # Min 7pt
            self.dynamic_header_font_size = max(self.base_header_font_size * reduction_factor, 10) # Min 10pt
            self.dynamic_table_font_size = max(self.table_font_size * reduction_factor, 7) # Min 7pt
            # Reduce row height less aggressively
            self.dynamic_row_height = max(self.base_row_height * (reduction_factor + 0.1), 0.8) # Min 0.8cm
            self.spacing_factor = max(reduction_factor, 0.4) # Min 0.4 factor
            # print(f"Adjusting sizes for {num_elements} elements. Factor: {reduction_factor:.2f}")
        else:
            # Use base sizes if enough space or few elements
            self.dynamic_font_size = self.base_font_size
            self.dynamic_header_font_size = self.base_header_font_size
            self.dynamic_table_font_size = self.table_font_size
            self.dynamic_row_height = self.base_row_height
            self.spacing_factor = 1.0
            # print(f"Using base sizes for {num_elements} elements.")

    def _set_cell_shading(self, cell, fill_color):
        """Helper to set cell background color."""
        try:
            shading_elm = parse_xml(f'<w:shd {nsdecls("w")} w:fill="{fill_color}"/>')
            cell._tc.get_or_add_tcPr().append(shading_elm)
        except Exception as e:
            print(f"Error setting cell shading: {e}")

    def _configure_cell(self, cell, text, bold=False, italic=False, font_size=None,
                         color_rgb=None, alignment=WD_ALIGN_PARAGRAPH.LEFT,
                         v_alignment=WD_ALIGN_VERTICAL.CENTER):
        """Helper to configure cell text and formatting."""
        cell.text = text
        cell.vertical_alignment = v_alignment
        paragraph = cell.paragraphs[0]
        paragraph.alignment = alignment
        paragraph.paragraph_format.space_before = Pt(0)
        paragraph.paragraph_format.space_after = Pt(0)
        if paragraph.runs:
            run = paragraph.runs[0]
            run.bold = bold
            run.italic = italic
            if font_size:
                run.font.size = Pt(font_size)
            if color_rgb:
                run.font.color.rgb = color_rgb
        return paragraph # Return paragraph for potential further modification

    def ajouter_entete_colore(self):
        """Adds the colored header section."""
        try:
            header_paragraph = self.document.add_paragraph()
            header_paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER
            header_paragraph.space_after = Pt(6 * self.spacing_factor)
            header_run = header_paragraph.add_run("ÉVALUATION GYMNASTIQUE")
            header_run.bold = True
            header_run.font.size = Pt(self.dynamic_header_font_size)
            header_run.font.color.rgb = RGBColor(0, 32, 96) # Dark Blue

            header_table = self.document.add_table(rows=3, cols=2)
            header_table.style = 'Table Grid'; header_table.autofit = False

            page_width_cm = self.document.sections[0].page_width.cm; left_margin_cm = self.document.sections[0].left_margin.cm; right_margin_cm = self.document.sections[0].right_margin.cm
            available_table_width = page_width_cm - left_margin_cm - right_margin_cm
            col_widths = [available_table_width * 0.55, available_table_width * 0.45]
            for i, width in enumerate(col_widths):
                for cell in header_table.columns[i].cells: cell.width = Cm(width)

            row_height_cm = max(0.6, 0.8 * self.spacing_factor)
            for row in header_table.rows:
                row.height = Cm(row_height_cm)
                for cell in row.cells:
                    cell.vertical_alignment = WD_ALIGN_VERTICAL.CENTER
                    self._set_cell_shading(cell, "D9E2F3") # Light Blue Shading
                    for p in cell.paragraphs: p.paragraph_format.space_before = Pt(0); p.paragraph_format.space_after = Pt(0)

            header_info = [
                [("Centre d'examen: ", self.centre_examen), ("Examen: ", self.type_examen)],
                [("Série: ", self.serie), ("Établissement: ", self.etablissement)],
                [("Session: ", self.session), ("Candidat: ", self.nom_candidat)]
            ]
            text_color = RGBColor(0, 32, 96) # Dark Blue

            for r, row_data in enumerate(header_info):
                for c, (label, value) in enumerate(row_data):
                    cell = header_table.cell(r, c)
                    p = cell.paragraphs[0]; p.clear()
                    run_label = p.add_run(label); run_label.bold = True; run_label.font.size = Pt(self.dynamic_font_size); run_label.font.color.rgb = text_color
                    run_value = p.add_run(value); run_value.font.size = Pt(self.dynamic_font_size)

            self.document.add_paragraph().paragraph_format.space_after = Pt(4 * self.spacing_factor)

        except Exception as e:
            print(f"Error adding header: {e}")
            traceback.print_exc()


    def creer_tableau_elements(self):
        """Creates the main table for technical elements."""
        try:
            num_elements = len(self.elements_techniques)
            if num_elements == 0: return # Don't create if empty

            table = self.document.add_table(rows=num_elements + 1, cols=5)
            table.style = 'Table Grid'; table.alignment = WD_TABLE_ALIGNMENT.CENTER; table.autofit = False

            page_width_cm = self.document.sections[0].page_width.cm; left_margin_cm = self.document.sections[0].left_margin.cm; right_margin_cm = self.document.sections[0].right_margin.cm
            available_table_width = page_width_cm - left_margin_cm - right_margin_cm
            total_prop = 8 + 3 + 2 + 2.5 + 2.5
            col_widths_cm = [available_table_width * (p / total_prop) for p in [8, 3, 2, 2.5, 2.5]]
            for i, width in enumerate(col_widths_cm):
                for cell in table.columns[i].cells: cell.width = Cm(width)

            min_row_height_cm = max(0.8, self.dynamic_row_height)
            for row in table.rows:
                row.height_rule = WD_ROW_HEIGHT_RULE.AT_LEAST; row.height = Cm(min_row_height_cm)
                for cell in row.cells:
                    cell.vertical_alignment = WD_ALIGN_VERTICAL.CENTER
                    for p in cell.paragraphs: p.paragraph_format.space_before = Pt(0); p.paragraph_format.space_after = Pt(0)

            header_row = table.rows[0]
            header_color = RGBColor(0, 32, 96)
            headers_config = [
                ("ELEMENTS TECHNIQUES", {}),
                ("CATEGORIES D'ELEMENTS TECHNIQUES ET PONDERATION", {"bold": True, "font_size": self.dynamic_table_font_size, "color_rgb": header_color, "alignment": WD_ALIGN_PARAGRAPH.CENTER}),
                ("", {}), # Merged cell placeholder
                ("APPRECIATIONS", {"bold": True, "font_size": self.dynamic_table_font_size, "color_rgb": header_color, "alignment": WD_ALIGN_PARAGRAPH.CENTER}),
                ("POINTS Accordés", {"bold": True, "font_size": self.dynamic_table_font_size, "color_rgb": header_color, "alignment": WD_ALIGN_PARAGRAPH.CENTER})
            ]
            for i, (text, config) in enumerate(headers_config):
                 cell = header_row.cells[i]
                 self._set_cell_shading(cell, "BDD7EE") # Header Shading Blue
                 if i != 2: # Skip placeholder for merged cell text config
                      self._configure_cell(cell, text, **config)

            try: table.cell(0, 1).merge(table.cell(0, 2))
            except Exception as merge_err: print(f"Error merging header cells: {merge_err}")

            # Add element rows
            for i, element in enumerate(self.elements_techniques, 1):
                if i >= len(table.rows): continue # Safety check

                # Col 0: Element Name (with single newline)
                element_cell = table.cell(i, 0)
                self._configure_cell(element_cell, f'{element["nom"]}\n', # ADDED SINGLE \n
                                     font_size=self.dynamic_table_font_size,
                                     alignment=WD_ALIGN_PARAGRAPH.LEFT,
                                     v_alignment=WD_ALIGN_VERTICAL.CENTER) # Center align vertically

                # Col 1: Category
                self._configure_cell(table.cell(i, 1), element["categorie"], bold=True, italic=True,
                                     font_size=self.dynamic_table_font_size, alignment=WD_ALIGN_PARAGRAPH.CENTER)

                # Col 2: Points Max
                self._configure_cell(table.cell(i, 2), str(element["points"]), bold=True, italic=True,
                                     font_size=self.dynamic_table_font_size, alignment=WD_ALIGN_PARAGRAPH.CENTER)

                # Col 3: Appreciations (Empty)
                self._configure_cell(table.cell(i, 3), "", font_size=self.dynamic_table_font_size)
                # Col 4: Points Accordés (Empty)
                self._configure_cell(table.cell(i, 4), "", font_size=self.dynamic_table_font_size)

            self.document.add_paragraph().paragraph_format.space_after = Pt(6 * self.spacing_factor)

        except Exception as e:
            print(f"Error creating elements table: {e}")
            traceback.print_exc()


    def ajouter_note_jury(self):
        """Adds the NB1 note for the jury."""
        try:
            para = self.document.add_paragraph(); para.paragraph_format.space_before = Pt(4 * self.spacing_factor); para.paragraph_format.space_after = Pt(4 * self.spacing_factor)
            run = para.add_run("NB1 : Zone réservée aux membres du jury ! Le jury cochera le point correspondant au niveau de réalisation de l'élément gymnique par le candidat.")
            run.bold = True; run.font.color.rgb = RGBColor(255, 0, 0); run.font.size = Pt(max(self.dynamic_font_size - 2, 6)) # Min 6pt
        except Exception as e:
            print(f"Error adding jury note: {e}")


    def creer_tableau_recapitulatif(self):
        """Creates the summary table for scores."""
        try:
            note_table = self.document.add_table(rows=3, cols=13); note_table.style = 'Table Grid'; note_table.alignment = WD_TABLE_ALIGNMENT.CENTER; note_table.autofit = False

            page_width_cm = self.document.sections[0].page_width.cm; left_margin_cm = self.document.sections[0].left_margin.cm; right_margin_cm = self.document.sections[0].right_margin.cm
            available_recap_width = page_width_cm - left_margin_cm - right_margin_cm
            width_A_E_pair = available_recap_width * (1.2 / 13.0); width_final_single = available_recap_width * (1.0 / 13.0)
            col_widths_recap = [];
            for _ in range(5): col_widths_recap.extend([width_A_E_pair / 2, width_A_E_pair / 2])
            col_widths_recap.extend([width_final_single, width_final_single, width_final_single])
            current_total_width = sum(col_widths_recap); width_adjustment = (available_recap_width - current_total_width) / len(col_widths_recap)
            for i, width in enumerate(col_widths_recap):
                adjusted_width = max(0.5, width + width_adjustment);
                for cell in note_table.columns[i].cells: cell.width = Cm(adjusted_width)

            row_height_cm = max(0.5, 0.6 * self.spacing_factor)
            for row in note_table.rows:
                row.height = Cm(row_height_cm)
                for cell in row.cells: cell.vertical_alignment = WD_ALIGN_VERTICAL.CENTER;
                for p in cell.paragraphs: p.paragraph_format.space_before = Pt(0); p.paragraph_format.space_after = Pt(0)

            header_color = RGBColor(0, 32, 96)
            header_fill = "BDD7EE"
            recap_font_size = max(self.dynamic_table_font_size - 1, 6) # Min 6pt

            for cell in note_table.rows[0].cells: self._set_cell_shading(cell, header_fill)

            type_data = [("A", "1pt"), ("B", "1,5pt"), ("C", "2pts"), ("D", "2,5pts"), ("E", "3pts")]
            for col, (type_lettre, points) in enumerate(type_data):
                idx = col * 2
                if idx + 1 < 13:
                    cell = note_table.cell(0, idx)
                    try:
                        cell.merge(note_table.cell(0, idx + 1))
                        self._configure_cell(cell, f"Type {type_lettre}\n{points}", bold=True, font_size=recap_font_size, color_rgb=header_color, alignment=WD_ALIGN_PARAGRAPH.CENTER)
                    except Exception as e: print(f"Error merging recap cells at index {idx}: {e}")

            final_headers = [("ROV", "2pts"), ("Projet", "2pts"), ("Réalisation", "16pts")]
            for col_offset, (titre, points) in enumerate(final_headers):
                col = 10 + col_offset
                if col < 13:
                    self._configure_cell(note_table.cell(0, col), f"{titre}\n{points}", bold=True, font_size=recap_font_size, color_rgb=header_color, alignment=WD_ALIGN_PARAGRAPH.CENTER)

            # Row 2: NEG / Note labels
            for col in range(5):
                idx = col * 2
                if idx + 1 < 13:
                    self._configure_cell(note_table.cell(1, idx), "NEG", italic=True, font_size=recap_font_size, alignment=WD_ALIGN_PARAGRAPH.CENTER)
                    self._configure_cell(note_table.cell(1, idx + 1), "Note", italic=True, font_size=recap_font_size, alignment=WD_ALIGN_PARAGRAPH.CENTER)
            for col in range(10, 13):
                 if col < 13:
                      self._configure_cell(note_table.cell(1, col), "Note", italic=True, font_size=recap_font_size, alignment=WD_ALIGN_PARAGRAPH.CENTER)

            # Row 3 is left empty for scores

            self.document.add_paragraph().paragraph_format.space_after = Pt(6 * self.spacing_factor)

        except Exception as e:
            print(f"Error creating recap table: {e}")
            traceback.print_exc()


    def ajouter_note_candidat_avec_cadre(self):
        """Adds the NB2 instructions in a green box."""
        try:
            note_table = self.document.add_table(rows=1, cols=1); note_table.style = 'Table Grid'; note_table.alignment = WD_TABLE_ALIGNMENT.CENTER; note_table.autofit = True
            cell = note_table.cell(0, 0); self._set_cell_shading(cell, "C6E0B4") # Light Green Shading
            p = cell.paragraphs[0]; p.paragraph_format.space_before = Pt(2); p.paragraph_format.space_after = Pt(2)
            font_size = max(7 * self.spacing_factor, 6) # Min 6pt for readability
            run = p.add_run("NB2: Après le choix des catégories d'éléments gymniques par le candidat, ce dernier remplira la colonne de pointage selon l'orientation suivante: A (0.25; 0.5; 0.75; 1) B (0.25; 0.5; 0.75; 1; 1.25; 1.5) C (0.5; 0.75; 1; 1.25; 1.5; 2) D (0.75; 1; 1.25; 1.5; 2; 2.5) et E (0.75; 1; 1.5; 2; 2.5; 3) également, le candidat devra fournir 2 copies de son projet sur une page! (appréciations: NR, NM, PM, M).")
            run.italic = True; run.font.size = Pt(font_size)
            self.document.add_paragraph().paragraph_format.space_after = Pt(8 * self.spacing_factor)
        except Exception as e:
            print(f"Error adding candidate note box: {e}")


    def ajouter_zone_note(self):
        """Adds the 'Note finale/20' label and the empty score box."""
        try:
            para_note_label = self.document.add_paragraph(); para_note_label.alignment = WD_ALIGN_PARAGRAPH.RIGHT
            para_note_label.paragraph_format.space_after = Pt(1); para_note_label.paragraph_format.space_before = Pt(4 * self.spacing_factor)
            run = para_note_label.add_run("Note finale/20"); run.bold = True; run.font.size = Pt(self.dynamic_table_font_size + 1); run.font.color.rgb = RGBColor(0, 32, 96)

            box_table = self.document.add_table(rows=1, cols=1); box_table.style = 'Table Grid'; box_table.alignment = WD_TABLE_ALIGNMENT.RIGHT
            box_size = Cm(1.5); cell = box_table.cell(0, 0); cell.width = box_size; box_table.rows[0].height = box_size
            self._configure_cell(cell, "", v_alignment=WD_ALIGN_VERTICAL.CENTER) # Ensure empty and centered
            # Optional shading for the box: self._set_cell_shading(cell, "F2F2F2") # Light Gray

            self.document.add_paragraph().paragraph_format.space_after = Pt(8 * self.spacing_factor)
        except Exception as e:
            print(f"Error adding final score zone: {e}")


    def ajouter_lignes_correcteurs(self):
        """Adds lines for corrector signatures."""
        try:
            num_elements = len(self.elements_techniques); use_compact_mode = num_elements > 12
            if use_compact_mode:
                para = self.document.add_paragraph(); para.paragraph_format.space_before = Pt(4 * self.spacing_factor); para.paragraph_format.space_after = Pt(4 * self.spacing_factor)
                run = para.add_run("Correcteurs: "); run.bold = True; run.font.size = Pt(self.dynamic_font_size); para.add_run("Projet / Principal / ROV").font.size = Pt(self.dynamic_font_size); para.add_run("\n" + "." * 30)
            else:
                for role in ["Projet", "Principal", "ROV"]:
                    para = self.document.add_paragraph(); para.paragraph_format.space_before = Pt(3 * self.spacing_factor); para.paragraph_format.space_after = Pt(1 * self.spacing_factor)
                    run = para.add_run(f"Correcteur {role} : "); run.bold = True; run.font.size = Pt(self.dynamic_font_size)
                    chars_per_cm_estimate = 3; line_length_cm = 10; points_count = int(line_length_cm * chars_per_cm_estimate)
                    points_count = max(20, points_count); points_count = int(points_count * (self.dynamic_font_size / 10.0) * self.spacing_factor); points_count = max(15, points_count)
                    para.add_run("." * points_count).font.size = Pt(self.dynamic_font_size)
        except Exception as e:
             print(f"Error adding corrector lines: {e}")


    def modifier_centre_examen(self, nom): self.centre_examen = nom
    def modifier_type_examen(self, type_examen): self.type_examen = type_examen
    def modifier_serie(self, serie): self.serie = serie
    def modifier_etablissement(self, nom): self.etablissement = nom
    def modifier_session(self, annee): self.session = annee
    def modifier_candidat(self, nom): self.nom_candidat = nom

    def ajouter_element(self, nom, categorie, points):
        try: point_value = float(str(points).replace(',', '.')) # Ensure conversion from string, handle comma
        except (ValueError, TypeError): print(f"Warning: Invalid points value '{points}' for element '{nom}'. Using 0.0."); point_value = 0.0
        self.elements_techniques.append({"nom": nom, "categorie": categorie, "points": point_value})

    def generer_document(self, nom_fichier="evaluation_gymnastique.docx"):
        """Generates the complete Word document."""
        try:
            self.calculate_dynamic_sizing()
            self.ajouter_entete_colore()
            self.creer_tableau_elements() # Includes single newline after name
            self.ajouter_note_jury()
            self.creer_tableau_recapitulatif()
            self.ajouter_lignes_correcteurs() # Before final score box usually
            self.ajouter_zone_note()
            self.ajouter_note_candidat_avec_cadre() # Keep green box near the end

            self.document.save(nom_fichier)
            print(f"Document '{nom_fichier}' generated successfully.")
            return nom_fichier
        except Exception as e:
            print(f"FATAL error generating document: {e}")
            traceback.print_exc()
            return None # Indicate failure

# --- Flask Application ---
app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
app.secret_key = os.urandom(24) # Needed for flash messages

@app.route("/eps", methods=["GET", "POST"])
def index():
    if request.method == "POST":
        docx_filepath = None # Initialize variable
        pdf_filepath = None # Initialize variable
        try:
            # --- Récupération des informations ---
            centre_examen = request.form.get("centre_examen", "Centre d'examen")
            type_examen = request.form.get("type_examen", "Bac Général")
            serie = request.form.get("serie", "Série")
            etablissement = request.form.get("etablissement", "Établissement")
            session_value = request.form.get("session", "2025")
            nom_candidat = request.form.get("nom_candidat", "Candidat")
            output_format = request.form.get("format", "docx")

            # --- Création et configuration du document ---
            evaluation = EvaluationGymnique()
            evaluation.modifier_centre_examen(centre_examen); evaluation.modifier_type_examen(type_examen)
            evaluation.modifier_serie(serie); evaluation.modifier_etablissement(etablissement)
            evaluation.modifier_session(session_value); evaluation.modifier_candidat(nom_candidat)

            # --- Récupération des éléments techniques ---
            element_names = request.form.getlist("new_element_name")
            element_categories = request.form.getlist("new_element_categorie")
            element_points = request.form.getlist("new_element_points")

            num_elements_added = 0
            for name, cat, pts in zip(element_names, element_categories, element_points):
                if name and name.strip() and cat and cat.strip() and pts and pts.strip():
                    evaluation.ajouter_element(name.strip(), cat.strip(), pts.strip())
                    num_elements_added += 1
                else:
                    # Optionally log skipped incomplete elements for debugging
                    # print(f"Skipping incomplete element: Name='{name}', Cat='{cat}', Pts='{pts}'")
                    pass # Silently skip incomplete rows

            if num_elements_added == 0:
                 flash("Aucun élément technique complet n'a été fourni. Le document sera généré sans éléments.", "warning")
                 # Allow generation even with no elements, or redirect back:
                 # return redirect(url_for('index'))

            # --- Génération du document DOCX ---
            safe_candidat_name = "".join(c if c.isalnum() else "_" for c in nom_candidat) # Sanitize name for filename
            safe_session = "".join(c if c.isalnum() else "_" for c in session_value)
            base_filename = f"evaluation_{safe_candidat_name}_{safe_session}"
            docx_filename = f"{base_filename}.docx"
            docx_filepath = os.path.join(app.config['UPLOAD_FOLDER'], docx_filename)

            generated_docx = evaluation.generer_document(docx_filepath)

            if not generated_docx: # Check if generation failed
                flash("Une erreur est survenue lors de la génération du document DOCX.", "error")
                return redirect(url_for('index'))

            # --- Conversion et envoi ---
            if output_format == "pdf":
                 print(f"Attempting PDF conversion for {docx_filepath}...")
                 try:
                    # Check API key again before conversion attempt
                    if not convertapi.api_secret or convertapi.api_secret == 'YOUR_SECRET':
                         flash("La clé API pour ConvertAPI n'est pas configurée correctement. Impossible de générer le PDF.", "error")
                         # Optionally send the docx as fallback?
                         # return send_file(docx_filepath, as_attachment=True, download_name=docx_filename)
                         return redirect(url_for('index'))

                    result = convertapi.convert('pdf', { 'File': docx_filepath }, from_format = 'docx')
                    pdf_filename_base = f"{base_filename}.pdf"
                    pdf_filepath = os.path.join(app.config['UPLOAD_FOLDER'], pdf_filename_base)
                    result.save_files(pdf_filepath)
                    print(f"PDF saved to {pdf_filepath}")
                    # Send the generated PDF
                    return send_file(pdf_filepath, as_attachment=True, download_name=pdf_filename_base)

                 except convertapi.ApiError as api_err:
                      print(f"ConvertAPI Error: {api_err}")
                      flash(f"Erreur ConvertAPI: {api_err}. Vérifiez votre clé ou vos crédits. Le fichier DOCX sera téléchargé.", "warning")
                      # Fallback to sending DOCX
                      return send_file(docx_filepath, as_attachment=True, download_name=docx_filename)
                 except Exception as e:
                      print(f"Error during PDF conversion: {e}")
                      traceback.print_exc()
                      flash(f"Erreur lors de la conversion PDF: {e}. Le fichier DOCX sera téléchargé.", "warning")
                      # Fallback to sending DOCX
                      return send_file(docx_filepath, as_attachment=True, download_name=docx_filename)

            else: # Send the generated DOCX
                return send_file(docx_filepath, as_attachment=True, download_name=docx_filename)

        except Exception as e:
            print(f"An error occurred during POST request processing: {e}")
            traceback.print_exc()
            flash(f"Une erreur interne est survenue: {e}", "error")
            return redirect(url_for('index')) # Redirect back to form on error

        finally:
             # Clean up temporary files (optional, consider delay or cron job for robustness)
             # Be careful cleaning up docx if PDF failed and you sent docx as fallback
             if output_format == "pdf" and pdf_filepath and os.path.exists(pdf_filepath):
                  # If PDF was successfully sent (or attempted), remove the source docx
                  if docx_filepath and os.path.exists(docx_filepath):
                       try: os.remove(docx_filepath)
                       except OSError as e: print(f"Error removing temp docx: {e}")
                  # Maybe remove the PDF too after sending? Or keep it for a while?
                  # try: os.remove(pdf_filepath)
                  # except OSError as e: print(f"Error removing temp pdf: {e}")
             elif output_format == "docx" and docx_filepath and os.path.exists(docx_filepath):
                  # Maybe remove the DOCX after sending? Or keep it? Let's keep it for now.
                  pass


    # --- Affichage du formulaire (GET request) ---
    return render_template("index.html")

if __name__ == "__main__":
    # Make sure the UPLOAD_FOLDER exists when running directly
    if not os.path.exists(UPLOAD_FOLDER):
        try:
             os.makedirs(UPLOAD_FOLDER)
        except OSError as e:
             print(f"CRITICAL: Could not create upload folder '{UPLOAD_FOLDER}'. Exiting. Error: {e}")
             exit() # Exit if we can't create the folder

    # Consider security implications of debug=True in production
    app.run(debug=True, host='0.0.0.0', port=5001) # Run on port 5001, accessible on network