File size: 27,346 Bytes
e85cb9c
 
662cf94
e28d5a2
662cf94
 
 
e28d5a2
662cf94
 
9f7873d
e85cb9c
662cf94
e28d5a2
 
 
e85cb9c
 
 
 
 
e28d5a2
 
662cf94
e28d5a2
e85cb9c
662cf94
 
 
e85cb9c
662cf94
 
 
 
 
 
e28d5a2
e85cb9c
662cf94
 
 
 
 
 
 
e85cb9c
 
e28d5a2
 
e85cb9c
 
e28d5a2
 
e85cb9c
 
 
 
e28d5a2
 
 
 
 
 
9f7873d
 
e85cb9c
 
 
e28d5a2
 
e85cb9c
e28d5a2
 
 
e85cb9c
 
 
9f7873d
 
 
 
 
 
e85cb9c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e28d5a2
662cf94
e85cb9c
 
e28d5a2
 
e85cb9c
e28d5a2
 
e85cb9c
e28d5a2
e85cb9c
662cf94
 
e85cb9c
 
 
 
 
 
 
 
e28d5a2
 
e85cb9c
662cf94
e28d5a2
 
 
 
e85cb9c
e28d5a2
 
 
e85cb9c
662cf94
e28d5a2
 
 
e85cb9c
 
662cf94
e85cb9c
 
662cf94
 
e85cb9c
 
e28d5a2
e85cb9c
662cf94
e28d5a2
e85cb9c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e28d5a2
e85cb9c
e28d5a2
662cf94
e85cb9c
 
e28d5a2
662cf94
e28d5a2
 
e85cb9c
e28d5a2
 
 
 
 
 
 
 
e85cb9c
662cf94
e28d5a2
 
e85cb9c
 
9f7873d
e85cb9c
e28d5a2
 
662cf94
 
 
e28d5a2
e85cb9c
 
e28d5a2
 
 
 
662cf94
e85cb9c
 
662cf94
 
 
e85cb9c
 
662cf94
 
e85cb9c
e28d5a2
 
662cf94
e28d5a2
 
e85cb9c
662cf94
e85cb9c
 
e28d5a2
 
662cf94
e85cb9c
 
e28d5a2
 
e85cb9c
e28d5a2
 
662cf94
e28d5a2
 
e85cb9c
 
 
9f7873d
e28d5a2
e85cb9c
 
 
 
e28d5a2
e85cb9c
e28d5a2
 
 
 
 
 
662cf94
e28d5a2
 
 
 
662cf94
e85cb9c
 
 
 
 
 
 
 
 
 
e28d5a2
 
e85cb9c
 
 
 
662cf94
e28d5a2
662cf94
e28d5a2
e85cb9c
662cf94
e85cb9c
662cf94
 
e85cb9c
 
e28d5a2
 
 
 
 
 
 
e85cb9c
 
 
 
 
 
 
e28d5a2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e85cb9c
e28d5a2
e85cb9c
 
 
 
 
 
 
 
e28d5a2
e85cb9c
 
 
 
 
 
e28d5a2
 
 
e85cb9c
 
 
 
 
 
 
 
e28d5a2
 
 
e85cb9c
 
 
 
 
e28d5a2
e85cb9c
e28d5a2
e85cb9c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e28d5a2
 
e85cb9c
 
 
e28d5a2
 
e85cb9c
e28d5a2
 
 
 
e85cb9c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e28d5a2
 
662cf94
 
 
e28d5a2
 
 
e85cb9c
 
e28d5a2
 
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
# --- START OF FLASK APP SCRIPT (v5 - Final) ---
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 uuid # For unique filenames

# --- Configuration ---
# IMPORTANT: Replace 'YOUR_SECRET' with your actual ConvertAPI secret
# You can get one from https://www.convertapi.com/a
convertapi.api_secret = 'secret_8wCI6pgOP9AxLVJG'

# Define a temporary directory for generated files
BASE_DIR = os.path.abspath(os.path.dirname(__file__))
UPLOAD_FOLDER = os.path.join(BASE_DIR, 'temp_files')
if not os.path.exists(UPLOAD_FOLDER):
    os.makedirs(UPLOAD_FOLDER)


# --- Classe de génération de document (Modifiée pour interligne standard) ---
class EvaluationGymnique:
    def __init__(self):
        self.document = Document()
        # --- Document Setup (Margins, etc.) ---
        self.document.sections[0].page_height = Cm(29.7)
        self.document.sections[0].page_width = Cm(21)
        self.document.sections[0].left_margin = Cm(1.5)
        self.document.sections[0].right_margin = Cm(1.5)
        self.document.sections[0].top_margin = Cm(1)
        self.document.sections[0].bottom_margin = Cm(1)

        # --- Default Data ---
        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 = []

        # --- Layout Parameters ---
        self.base_font_size = 10
        self.base_header_font_size = 14
        # Adjusted base row height slightly for single line break comfort
        self.base_row_height = 1.0
        self.table_font_size = 9
        self.available_height = 27.7
        # Adjusted fixed height estimate slightly
        self.fixed_elements_height = 14

        # --- Dynamic Parameters (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):
        num_elements = len(self.elements_techniques)
        # Estimate accounts for single line break now
        estimated_table_height = (num_elements + 1) * self.base_row_height * 1.3 # Reduced multiplier

        available_space_for_table = self.available_height - self.fixed_elements_height
        if estimated_table_height > available_space_for_table and num_elements > 0:
            reduction_factor = max(0.6, 1 - (max(0, num_elements - 10) * 0.04)) # Adjusted factor/threshold
            self.dynamic_font_size = max(self.base_font_size * reduction_factor, 6)
            self.dynamic_header_font_size = max(self.base_header_font_size * reduction_factor, 9)
            self.dynamic_table_font_size = max(self.table_font_size * reduction_factor, 6)
            self.dynamic_row_height = max(self.base_row_height * (reduction_factor + 0.1), 0.7) # Min 0.7cm
            self.spacing_factor = max(reduction_factor, 0.4)
            print(f"Adjusting sizes for {num_elements} elements. Factor: {reduction_factor:.2f}") # Optional debug
        else:
            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.") # Optional debug

    def set_paragraph_format(self, paragraph, size=None, bold=False, italic=False, color_rgb=None, align=None, space_before=0, space_after=0):
        """Helper to format paragraphs and their first run."""
        paragraph.paragraph_format.space_before = Pt(space_before)
        paragraph.paragraph_format.space_after = Pt(space_after)
        if align is not None:
            paragraph.alignment = align
        if paragraph.runs:
            run = paragraph.runs[0]
            if size: run.font.size = Pt(size)
            run.bold = bold
            run.italic = italic
            if color_rgb: run.font.color.rgb = color_rgb
        # Consider adding a run if none exists, though setting text usually creates one.

    def ajouter_entete_colore(self):
        p = self.document.add_paragraph(); p.add_run("ÉVALUATION GYMNASTIQUE")
        self.set_paragraph_format(p, size=self.dynamic_header_font_size, bold=True, color_rgb=RGBColor(0, 32, 96), align=WD_ALIGN_PARAGRAPH.CENTER, space_after=6 * self.spacing_factor)
        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)
        shading_fill = "D9E2F3"
        for row in header_table.rows:
            for cell in row.cells:
                cell.vertical_alignment = WD_ALIGN_VERTICAL.CENTER; shading_elm = parse_xml(f'<w:shd {nsdecls("w")} w:fill="{shading_fill}"/>'); cell._tc.get_or_add_tcPr().append(shading_elm)
                for p in cell.paragraphs: self.set_paragraph_format(p) # Ensure 0 spacing inside cells
        # Fill header data using helper
        def fill_header_cell(cell, label, value):
            p = cell.paragraphs[0]; p.clear(); run = p.add_run(label); run.bold = True; run.font.size = Pt(self.dynamic_font_size); run.font.color.rgb = RGBColor(0, 32, 96); p.add_run(value).font.size = Pt(self.dynamic_font_size)
        fill_header_cell(header_table.cell(0, 0), "Centre d'examen: ", self.centre_examen); fill_header_cell(header_table.cell(0, 1), "Examen: ", self.type_examen)
        fill_header_cell(header_table.cell(1, 0), "Série: ", self.serie); fill_header_cell(header_table.cell(1, 1), "Établissement: ", self.etablissement)
        fill_header_cell(header_table.cell(2, 0), "Session: ", self.session); fill_header_cell(header_table.cell(2, 1), "Candidat: ", self.nom_candidat)
        self.document.add_paragraph().paragraph_format.space_after = Pt(4 * self.spacing_factor)

    # --- MODIFIED METHOD for Line Breaks ---
    def creer_tableau_elements(self):
        num_elements = len(self.elements_techniques);
        if num_elements == 0: return
        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.7, self.dynamic_row_height) # Adjusted min 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
                 # Ensure default paragraph spacing is 0 unless overridden
                 for p in cell.paragraphs: self.set_paragraph_format(p)
        header_row = table.rows[0]
        shading_fill = "BDD7EE"
        for cell in header_row.cells: shading_elm = parse_xml(f'<w:shd {nsdecls("w")} w:fill="{shading_fill}"/>'); cell._tc.get_or_add_tcPr().append(shading_elm)
        headers = ["ELEMENTS TECHNIQUES", "CATEGORIES D'ELEMENTS TECHNIQUES ET PONDERATION", "", "APPRECIATIONS", "POINTS Accordés"]
        for i, header in enumerate(headers):
            cell = table.cell(0, i); p = cell.paragraphs[0]; p.clear(); p.add_run(header)
            self.set_paragraph_format(p, size=self.dynamic_table_font_size, bold=True, color_rgb=RGBColor(0, 32, 96), align=WD_ALIGN_PARAGRAPH.CENTER)
        try: table.cell(0, 1).merge(table.cell(0, 2))
        except Exception as e: print(f"Error merging cells: {e}")
        for i, element in enumerate(self.elements_techniques, 1):
            if i >= len(table.rows): continue
            # --- Element Name Cell ---
            element_cell = table.cell(i, 0)
            # Set text with a single newline
            element_cell.text = f'{element["nom"]}\n'
            element_cell.vertical_alignment = WD_ALIGN_VERTICAL.TOP # Align text to top
            # Format the paragraph containing the name and newline
            if element_cell.paragraphs:
                self.set_paragraph_format(element_cell.paragraphs[0], size=self.dynamic_table_font_size, bold=False, space_before=Pt(1), space_after=Pt(1)) # Minimal spacing

            # --- Other Cells ---
            def fill_element_cell(cell_index, text, align, is_bold=True, is_italic=True):
                 cell = table.cell(i, cell_index); cell.text = str(text) # Ensure text is string
                 if cell.paragraphs: self.set_paragraph_format(cell.paragraphs[0], size=self.dynamic_table_font_size, bold=is_bold, italic=is_italic, align=align)
            fill_element_cell(1, element["categorie"], WD_ALIGN_PARAGRAPH.CENTER)
            fill_element_cell(2, element["points"], WD_ALIGN_PARAGRAPH.CENTER)
            fill_element_cell(3, "", WD_ALIGN_PARAGRAPH.CENTER, is_bold=False, is_italic=False) # Appreciation
            fill_element_cell(4, "", WD_ALIGN_PARAGRAPH.CENTER, is_bold=False, is_italic=False) # Points Accordés

        self.document.add_paragraph().paragraph_format.space_after = Pt(6 * self.spacing_factor)
    # --- END OF MODIFIED METHOD ---

    def ajouter_note_jury(self):
        p = self.document.add_paragraph(); p.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.")
        self.set_paragraph_format(p, size=max(self.dynamic_font_size - 2, 5.5), bold=True, color_rgb=RGBColor(255, 0, 0), space_before=4 * self.spacing_factor, space_after=4 * self.spacing_factor)

    def creer_tableau_recapitulatif(self):
        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)
        shading_fill = "BDD7EE"
        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: self.set_paragraph_format(p)
        for cell in note_table.rows[0].cells: shading_elm = parse_xml(f'<w:shd {nsdecls("w")} w:fill="{shading_fill}"/>'); cell._tc.get_or_add_tcPr().append(shading_elm)
        recap_font_size = max(self.dynamic_table_font_size - 1, 5)
        header_color = RGBColor(0, 32, 96)
        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 < len(note_table.columns):
                cell = note_table.cell(0, idx)
                try:
                    cell.merge(note_table.cell(0, idx + 1)); p = cell.paragraphs[0]; p.clear(); p.add_run(f"Type {type_lettre}\n{points}")
                    self.set_paragraph_format(p, size=recap_font_size, bold=True, color_rgb=header_color, align=WD_ALIGN_PARAGRAPH.CENTER)
                except Exception as e: print(f"Error merging cells at index {idx}: {e}") # Optional logging
        final_headers = [("ROV", "2pts"), ("Projet", "2pts"), ("Réalisation", "16pts")]
        for col_offset, (titre, points) in enumerate(final_headers):
            col = 10 + col_offset
            if col < len(note_table.columns):
                cell = note_table.cell(0, col); p = cell.paragraphs[0]; p.clear(); p.add_run(f"{titre}\n{points}")
                self.set_paragraph_format(p, size=recap_font_size, bold=True, color_rgb=header_color, align=WD_ALIGN_PARAGRAPH.CENTER)
        for col in range(5):
            idx = col * 2
            if idx + 1 < len(note_table.columns):
                neg_cell = note_table.cell(1, idx); p_neg = neg_cell.paragraphs[0]; p_neg.clear(); p_neg.add_run("NEG"); self.set_paragraph_format(p_neg, size=recap_font_size, italic=True, align=WD_ALIGN_PARAGRAPH.CENTER)
                note_cell = note_table.cell(1, idx + 1); p_note = note_cell.paragraphs[0]; p_note.clear(); p_note.add_run("Note"); self.set_paragraph_format(p_note, size=recap_font_size, italic=True, align=WD_ALIGN_PARAGRAPH.CENTER)
        for col in range(10, 13):
            if col < len(note_table.columns):
                cell = note_table.cell(1, col); p = cell.paragraphs[0]; p.clear(); p.add_run("Note"); self.set_paragraph_format(p, size=recap_font_size, italic=True, align=WD_ALIGN_PARAGRAPH.CENTER)
        self.document.add_paragraph().paragraph_format.space_after = Pt(6 * self.spacing_factor)

    def ajouter_note_candidat_avec_cadre(self):
        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); shading_elm = parse_xml(f'<w:shd {nsdecls("w")} w:fill="C6E0B4"/>'); cell._tc.get_or_add_tcPr().append(shading_elm)
        p = cell.paragraphs[0]; p.clear()
        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(max(6 * self.spacing_factor, 5))
        p.paragraph_format.space_before = Pt(2); p.paragraph_format.space_after = Pt(2)
        self.document.add_paragraph().paragraph_format.space_after = Pt(8 * self.spacing_factor)

    def ajouter_zone_note(self):
        p_label = self.document.add_paragraph(); p_label.add_run("Note finale/20")
        self.set_paragraph_format(p_label, size=self.dynamic_table_font_size + 1, bold=True, color_rgb=RGBColor(0, 32, 96), align=WD_ALIGN_PARAGRAPH.RIGHT, space_before=4 * self.spacing_factor, space_after=1)
        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
        cell.vertical_alignment = WD_ALIGN_VERTICAL.CENTER; cell.text = ""; self.set_paragraph_format(cell.paragraphs[0])
        self.document.add_paragraph().paragraph_format.space_after = Pt(8 * self.spacing_factor)

    def ajouter_lignes_correcteurs(self):
        num_elements = len(self.elements_techniques); use_compact_mode = num_elements > 12
        if use_compact_mode:
            p = self.document.add_paragraph(); run = p.add_run("Correcteurs: "); run.bold = True; run.font.size = Pt(self.dynamic_font_size)
            p.add_run("Projet / Principal / ROV").font.size = Pt(self.dynamic_font_size); p.add_run("\n" + "." * 30)
            self.set_paragraph_format(p, space_before=4 * self.spacing_factor, space_after=4 * self.spacing_factor)
        else:
            for role in ["Projet", "Principal", "ROV"]:
                p = self.document.add_paragraph(); run = p.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 * (self.dynamic_font_size / 10.0) * self.spacing_factor); points_count = max(15, points_count)
                p.add_run("." * points_count).font.size = Pt(self.dynamic_font_size)
                self.set_paragraph_format(p, space_before=3 * self.spacing_factor, space_after=1 * self.spacing_factor)

    # --- Data Modifiers ---
    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(points)
        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 DOCX document."""
        self.calculate_dynamic_sizing()
        self.ajouter_entete_colore()
        self.creer_tableau_elements()
        self.ajouter_note_jury()
        self.creer_tableau_recapitulatif()
        # Reordered slightly for common layout flow
        self.ajouter_note_candidat_avec_cadre() # Instructions often come before signatures/final score
        self.ajouter_lignes_correcteurs()
        self.ajouter_zone_note()
        try:
            self.document.save(nom_fichier); print(f"Document '{nom_fichier}' generated successfully.")
            return nom_fichier
        except Exception as e:
            print(f"Error saving document: {e}")
            raise # Re-raise the exception for Flask to handle

# --- 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 to None
        pdf_filepath = None # Initialize to None
        try:
            # --- Récupération des informations depuis le formulaire ---
            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").strip()
            output_format = request.form.get("format", "docx")

            # Basic validation for candidate name
            if not nom_candidat:
                flash("Le nom du candidat ne peut pas être vide.", "error")
                return redirect(url_for('index'))

            # --- 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")

            valid_elements_added = 0
            for name, cat, pts in zip(element_names, element_categories, element_points):
                if name.strip() and cat.strip() and pts.strip():
                    try:
                         # Validate points format (allow dot or comma, convert to float)
                        pts_float = float(pts.replace(',', '.'))
                        evaluation.ajouter_element(name.strip(), cat.strip(), pts_float)
                        valid_elements_added += 1
                    except ValueError:
                         print(f"Skipping element due to invalid points format: Name='{name}', Pts='{pts}'")
                else:
                    print(f"Skipping incomplete element: Name='{name}', Cat='{cat}', Pts='{pts}'")

            if valid_elements_added == 0:
                flash("Aucun élément technique valide n'a été ajouté. Veuillez vérifier les entrées.", "error")
                # Persist form data (more advanced, not implemented here simply)
                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)
            unique_id = uuid.uuid4().hex[:6] # Short unique ID
            base_filename = f"evaluation_{safe_candidat_name}_{session_value}_{unique_id}"
            docx_filename = f"{base_filename}.docx"
            docx_filepath = os.path.join(app.config['UPLOAD_FOLDER'], docx_filename)

            # This might raise an exception if saving fails
            evaluation.generer_document(docx_filepath)

            # --- Conversion et envoi ---
            if output_format == "pdf":
                 if not convertapi.api_secret or convertapi.api_secret == 'YOUR_SECRET':
                      flash("La clé API pour ConvertAPI n'est pas configurée. Impossible de générer le PDF.", "error")
                      # Still have the DOCX, could offer that instead or redirect
                      # Let's redirect back for now
                      return redirect(url_for('index'))

                 print(f"Attempting PDF conversion for {docx_filepath}...")
                 try:
                    # Explicitly set output path for saved PDF
                    pdf_filename = f"{base_filename}.pdf"
                    pdf_filepath = os.path.join(app.config['UPLOAD_FOLDER'], pdf_filename)

                    result = convertapi.convert('pdf', {'File': docx_filepath}, from_format = 'docx')

                    # Save the converted file(s)
                    saved_files = result.save_files(app.config['UPLOAD_FOLDER']) # Save to temp folder
                    print(f"ConvertAPI saved files: {saved_files}")

                    # Find the expected PDF file path (ConvertAPI might rename slightly)
                    # Let's assume the first saved file is the PDF we want if only one exists
                    if saved_files and len(saved_files) == 1:
                         actual_pdf_path = saved_files[0]
                         # Optionally rename it back to our desired name if needed, but sending works
                         print(f"PDF conversion successful: {actual_pdf_path}")
                         return send_file(actual_pdf_path, as_attachment=True, download_name=pdf_filename)
                    elif os.path.exists(pdf_filepath): # Check if it saved with exact name
                         print(f"PDF conversion successful (exact path): {pdf_filepath}")
                         return send_file(pdf_filepath, as_attachment=True, download_name=pdf_filename)
                    else:
                        flash(f"Erreur: Fichier PDF non trouvé après conversion. Fichiers sauvegardés: {saved_files}", "error")
                        return redirect(url_for('index'))

                 except Exception as e:
                      print(f"Error during PDF conversion: {e}")
                      flash(f"Erreur durant la conversion PDF: {e}. Vérifiez vos crédits ConvertAPI ou le fichier.", "error")
                      # Fallback: Offer the DOCX instead? Or just redirect.
                      return redirect(url_for('index'))

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

        except Exception as e:
            # Log the general error
            print(f"An error occurred during POST request processing: {e}")
            flash(f"Une erreur interne est survenue: {e}", "error")
            return redirect(url_for('index'))

        finally:
            # --- Cleanup ---
            # Clean up DOCX only if PDF was successfully sent or if DOCX was sent
            # If PDF conversion failed but DOCX exists, maybe keep it?
            # Let's clean up the DOCX if PDF was *attempted* regardless of success for simplicity
            if output_format == 'pdf' and docx_filepath and os.path.exists(docx_filepath):
                 try:
                     os.remove(docx_filepath)
                     print(f"Cleaned up DOCX: {docx_filepath}")
                 except OSError as e:
                     print(f"Error cleaning up DOCX file {docx_filepath}: {e}")
            # We don't explicitly clean the PDF here as send_file handles it (or it might fail before sending)


    # --- 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):
        os.makedirs(UPLOAD_FOLDER)
    # Consider security implications of running debug=True in production
    app.run(debug=True, host='0.0.0.0', port=5001) # Run on port 5001 for example

# --- END OF FLASK APP SCRIPT ---