Docfile commited on
Commit
45216d7
·
verified ·
1 Parent(s): 133cfc6

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +129 -320
app.py CHANGED
@@ -1,4 +1,4 @@
1
- # --- START OF COMPLETE FLASK APP SCRIPT (v5) ---
2
  from flask import Flask, render_template, request, send_file, flash, redirect, url_for
3
  import os
4
  import convertapi # For PDF conversion
@@ -12,9 +12,9 @@ import math
12
  import traceback # For detailed error logging
13
 
14
  # --- Configuration ---
15
- # Use the provided ConvertAPI secret
16
- convertapi.api_secret = 'secret_8wCI6pgOP9AxLVJG'
17
  convertapi.api_credentials = 'secret_8wCI6pgOP9AxLVJG'
 
18
  # Define a temporary directory for generated files
19
  UPLOAD_FOLDER = 'temp_files'
20
  if not os.path.exists(UPLOAD_FOLDER):
@@ -22,242 +22,128 @@ if not os.path.exists(UPLOAD_FOLDER):
22
  os.makedirs(UPLOAD_FOLDER)
23
  except OSError as e:
24
  print(f"Error creating directory {UPLOAD_FOLDER}: {e}")
25
- # Handle the error appropriately, maybe exit or use a default path
26
 
27
 
28
- # --- Classe de génération de document (v5 - Finalized for Flask) ---
29
  class EvaluationGymnique:
30
  def __init__(self):
31
  self.document = Document()
32
- # --- Document Setup (Margins, Page Size) ---
33
  try:
34
  section = self.document.sections[0]
35
- section.page_height = Cm(29.7)
36
- section.page_width = Cm(21)
37
- section.left_margin = Cm(1.5)
38
- section.right_margin = Cm(1.5)
39
- section.top_margin = Cm(1)
40
- section.bottom_margin = Cm(1)
41
- except Exception as e:
42
- print(f"Error setting up document sections: {e}")
43
-
44
-
45
- # --- Default Properties ---
46
- self.centre_examen = "Centre d'examen"
47
- self.type_examen = "Bac Général"
48
- self.serie = "Série"
49
- self.etablissement = "Établissement"
50
- self.session = "2025"
51
- self.nom_candidat = "Candidat"
52
  self.elements_techniques = []
53
- self.appreciations = ["M", "PM", "NM", "NR"] # Not directly used in output, but good to have
54
-
55
- # --- Layout Parameters ---
56
- self.base_font_size = 10
57
- self.base_header_font_size = 14
58
- self.base_row_height = 1.1 # Adjusted base height
59
- self.table_font_size = 9
60
- self.available_height = 27.7 # A4 height minus margins
61
- self.fixed_elements_height = 15 # Estimated height of non-main-table elements
62
-
63
- # --- Dynamic Sizes (Initialized) ---
64
- self.dynamic_font_size = self.base_font_size
65
- self.dynamic_header_font_size = self.base_header_font_size
66
- self.dynamic_table_font_size = self.table_font_size
67
- self.dynamic_row_height = self.base_row_height
68
  self.spacing_factor = 1.0
69
 
70
  def calculate_dynamic_sizing(self):
71
- """Adjusts font sizes and spacing based on the number of elements."""
72
- num_elements = len(self.elements_techniques)
73
- # Estimate table height considering element name + 1 newline
74
- estimated_table_height = (num_elements + 1) * self.base_row_height * 1.3 # Moderate multiplier
75
-
76
  available_space_for_table = self.available_height - self.fixed_elements_height
77
-
78
- if estimated_table_height > available_space_for_table and num_elements > 5: # Start adjusting after 5 elements
79
- # More elements -> smaller sizes
80
- reduction_factor = max(0.6, 1 - (max(0, num_elements - 5) * 0.04)) # Gradual reduction
81
- self.dynamic_font_size = max(self.base_font_size * reduction_factor, 7) # Min 7pt
82
- self.dynamic_header_font_size = max(self.base_header_font_size * reduction_factor, 10) # Min 10pt
83
- self.dynamic_table_font_size = max(self.table_font_size * reduction_factor, 7) # Min 7pt
84
- # Reduce row height less aggressively
85
- self.dynamic_row_height = max(self.base_row_height * (reduction_factor + 0.1), 0.8) # Min 0.8cm
86
- self.spacing_factor = max(reduction_factor, 0.4) # Min 0.4 factor
87
- # print(f"Adjusting sizes for {num_elements} elements. Factor: {reduction_factor:.2f}")
88
  else:
89
- # Use base sizes if enough space or few elements
90
- self.dynamic_font_size = self.base_font_size
91
- self.dynamic_header_font_size = self.base_header_font_size
92
- self.dynamic_table_font_size = self.table_font_size
93
- self.dynamic_row_height = self.base_row_height
94
  self.spacing_factor = 1.0
95
- # print(f"Using base sizes for {num_elements} elements.")
96
 
97
  def _set_cell_shading(self, cell, fill_color):
98
- """Helper to set cell background color."""
99
  try:
100
  shading_elm = parse_xml(f'<w:shd {nsdecls("w")} w:fill="{fill_color}"/>')
101
  cell._tc.get_or_add_tcPr().append(shading_elm)
102
- except Exception as e:
103
- print(f"Error setting cell shading: {e}")
104
 
105
- def _configure_cell(self, cell, text, bold=False, italic=False, font_size=None,
106
- color_rgb=None, alignment=WD_ALIGN_PARAGRAPH.LEFT,
107
- v_alignment=WD_ALIGN_VERTICAL.CENTER):
108
- """Helper to configure cell text and formatting."""
109
- cell.text = text
110
- cell.vertical_alignment = v_alignment
111
- paragraph = cell.paragraphs[0]
112
- paragraph.alignment = alignment
113
- paragraph.paragraph_format.space_before = Pt(0)
114
- paragraph.paragraph_format.space_after = Pt(0)
115
  if paragraph.runs:
116
- run = paragraph.runs[0]
117
- run.bold = bold
118
- run.italic = italic
119
- if font_size:
120
- run.font.size = Pt(font_size)
121
- if color_rgb:
122
- run.font.color.rgb = color_rgb
123
- return paragraph # Return paragraph for potential further modification
124
 
125
  def ajouter_entete_colore(self):
126
- """Adds the colored header section."""
127
  try:
128
- header_paragraph = self.document.add_paragraph()
129
- header_paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER
130
- header_paragraph.space_after = Pt(6 * self.spacing_factor)
131
- header_run = header_paragraph.add_run("ÉVALUATION GYMNASTIQUE")
132
- header_run.bold = True
133
- header_run.font.size = Pt(self.dynamic_header_font_size)
134
- header_run.font.color.rgb = RGBColor(0, 32, 96) # Dark Blue
135
-
136
- header_table = self.document.add_table(rows=3, cols=2)
137
- header_table.style = 'Table Grid'; header_table.autofit = False
138
-
139
  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
140
  available_table_width = page_width_cm - left_margin_cm - right_margin_cm
141
  col_widths = [available_table_width * 0.55, available_table_width * 0.45]
142
  for i, width in enumerate(col_widths):
143
  for cell in header_table.columns[i].cells: cell.width = Cm(width)
144
-
145
  row_height_cm = max(0.6, 0.8 * self.spacing_factor)
146
  for row in header_table.rows:
147
  row.height = Cm(row_height_cm)
148
  for cell in row.cells:
149
- cell.vertical_alignment = WD_ALIGN_VERTICAL.CENTER
150
- self._set_cell_shading(cell, "D9E2F3") # Light Blue Shading
151
  for p in cell.paragraphs: p.paragraph_format.space_before = Pt(0); p.paragraph_format.space_after = Pt(0)
152
-
153
- header_info = [
154
- [("Centre d'examen: ", self.centre_examen), ("Examen: ", self.type_examen)],
155
- [("Série: ", self.serie), ("Établissement: ", self.etablissement)],
156
- [("Session: ", self.session), ("Candidat: ", self.nom_candidat)]
157
- ]
158
- text_color = RGBColor(0, 32, 96) # Dark Blue
159
-
160
  for r, row_data in enumerate(header_info):
161
  for c, (label, value) in enumerate(row_data):
162
- cell = header_table.cell(r, c)
163
- p = cell.paragraphs[0]; p.clear()
164
  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
165
  run_value = p.add_run(value); run_value.font.size = Pt(self.dynamic_font_size)
166
-
167
  self.document.add_paragraph().paragraph_format.space_after = Pt(4 * self.spacing_factor)
168
-
169
- except Exception as e:
170
- print(f"Error adding header: {e}")
171
- traceback.print_exc()
172
-
173
 
174
  def creer_tableau_elements(self):
175
- """Creates the main table for technical elements."""
176
  try:
177
- num_elements = len(self.elements_techniques)
178
- if num_elements == 0: return # Don't create if empty
179
-
180
- table = self.document.add_table(rows=num_elements + 1, cols=5)
181
- table.style = 'Table Grid'; table.alignment = WD_TABLE_ALIGNMENT.CENTER; table.autofit = False
182
-
183
  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
184
  available_table_width = page_width_cm - left_margin_cm - right_margin_cm
185
  total_prop = 8 + 3 + 2 + 2.5 + 2.5
186
  col_widths_cm = [available_table_width * (p / total_prop) for p in [8, 3, 2, 2.5, 2.5]]
187
  for i, width in enumerate(col_widths_cm):
188
  for cell in table.columns[i].cells: cell.width = Cm(width)
189
-
190
  min_row_height_cm = max(0.8, self.dynamic_row_height)
191
  for row in table.rows:
192
  row.height_rule = WD_ROW_HEIGHT_RULE.AT_LEAST; row.height = Cm(min_row_height_cm)
193
  for cell in row.cells:
194
  cell.vertical_alignment = WD_ALIGN_VERTICAL.CENTER
195
  for p in cell.paragraphs: p.paragraph_format.space_before = Pt(0); p.paragraph_format.space_after = Pt(0)
196
-
197
- header_row = table.rows[0]
198
- header_color = RGBColor(0, 32, 96)
199
- headers_config = [
200
- ("ELEMENTS TECHNIQUES", {}),
201
- ("CATEGORIES D'ELEMENTS TECHNIQUES ET PONDERATION", {"bold": True, "font_size": self.dynamic_table_font_size, "color_rgb": header_color, "alignment": WD_ALIGN_PARAGRAPH.CENTER}),
202
- ("", {}), # Merged cell placeholder
203
- ("APPRECIATIONS", {"bold": True, "font_size": self.dynamic_table_font_size, "color_rgb": header_color, "alignment": WD_ALIGN_PARAGRAPH.CENTER}),
204
- ("POINTS Accordés", {"bold": True, "font_size": self.dynamic_table_font_size, "color_rgb": header_color, "alignment": WD_ALIGN_PARAGRAPH.CENTER})
205
- ]
206
  for i, (text, config) in enumerate(headers_config):
207
- cell = header_row.cells[i]
208
- self._set_cell_shading(cell, "BDD7EE") # Header Shading Blue
209
- if i != 2: # Skip placeholder for merged cell text config
210
- self._configure_cell(cell, text, **config)
211
-
212
  try: table.cell(0, 1).merge(table.cell(0, 2))
213
  except Exception as merge_err: print(f"Error merging header cells: {merge_err}")
214
-
215
- # Add element rows
216
  for i, element in enumerate(self.elements_techniques, 1):
217
- if i >= len(table.rows): continue # Safety check
218
-
219
- # Col 0: Element Name (with single newline)
220
- element_cell = table.cell(i, 0)
221
- self._configure_cell(element_cell, f'{element["nom"]}\n', # ADDED SINGLE \n
222
- font_size=self.dynamic_table_font_size,
223
- alignment=WD_ALIGN_PARAGRAPH.LEFT,
224
- v_alignment=WD_ALIGN_VERTICAL.CENTER) # Center align vertically
225
-
226
- # Col 1: Category
227
- self._configure_cell(table.cell(i, 1), element["categorie"], bold=True, italic=True,
228
- font_size=self.dynamic_table_font_size, alignment=WD_ALIGN_PARAGRAPH.CENTER)
229
-
230
- # Col 2: Points Max
231
- self._configure_cell(table.cell(i, 2), str(element["points"]), bold=True, italic=True,
232
- font_size=self.dynamic_table_font_size, alignment=WD_ALIGN_PARAGRAPH.CENTER)
233
-
234
- # Col 3: Appreciations (Empty)
235
  self._configure_cell(table.cell(i, 3), "", font_size=self.dynamic_table_font_size)
236
- # Col 4: Points Accordés (Empty)
237
  self._configure_cell(table.cell(i, 4), "", font_size=self.dynamic_table_font_size)
238
-
239
  self.document.add_paragraph().paragraph_format.space_after = Pt(6 * self.spacing_factor)
240
-
241
- except Exception as e:
242
- print(f"Error creating elements table: {e}")
243
- traceback.print_exc()
244
-
245
 
246
  def ajouter_note_jury(self):
247
- """Adds the NB1 note for the jury."""
248
  try:
249
  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)
250
  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.")
251
- run.bold = True; run.font.color.rgb = RGBColor(255, 0, 0); run.font.size = Pt(max(self.dynamic_font_size - 2, 6)) # Min 6pt
252
- except Exception as e:
253
- print(f"Error adding jury note: {e}")
254
-
255
 
256
  def creer_tableau_recapitulatif(self):
257
- """Creates the summary table for scores."""
258
  try:
259
  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
260
-
261
  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
262
  available_recap_width = page_width_cm - left_margin_cm - right_margin_cm
263
  width_A_E_pair = available_recap_width * (1.2 / 13.0); width_final_single = available_recap_width * (1.0 / 13.0)
@@ -268,87 +154,57 @@ class EvaluationGymnique:
268
  for i, width in enumerate(col_widths_recap):
269
  adjusted_width = max(0.5, width + width_adjustment);
270
  for cell in note_table.columns[i].cells: cell.width = Cm(adjusted_width)
271
-
272
  row_height_cm = max(0.5, 0.6 * self.spacing_factor)
273
  for row in note_table.rows:
274
  row.height = Cm(row_height_cm)
275
  for cell in row.cells: cell.vertical_alignment = WD_ALIGN_VERTICAL.CENTER;
276
  for p in cell.paragraphs: p.paragraph_format.space_before = Pt(0); p.paragraph_format.space_after = Pt(0)
277
-
278
- header_color = RGBColor(0, 32, 96)
279
- header_fill = "BDD7EE"
280
- recap_font_size = max(self.dynamic_table_font_size - 1, 6) # Min 6pt
281
-
282
  for cell in note_table.rows[0].cells: self._set_cell_shading(cell, header_fill)
283
-
284
  type_data = [("A", "1pt"), ("B", "1,5pt"), ("C", "2pts"), ("D", "2,5pts"), ("E", "3pts")]
285
  for col, (type_lettre, points) in enumerate(type_data):
286
  idx = col * 2
287
  if idx + 1 < 13:
288
  cell = note_table.cell(0, idx)
289
  try:
290
- cell.merge(note_table.cell(0, idx + 1))
291
- 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)
292
  except Exception as e: print(f"Error merging recap cells at index {idx}: {e}")
293
-
294
  final_headers = [("ROV", "2pts"), ("Projet", "2pts"), ("Réalisation", "16pts")]
295
  for col_offset, (titre, points) in enumerate(final_headers):
296
  col = 10 + col_offset
297
- if col < 13:
298
- 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)
299
-
300
- # Row 2: NEG / Note labels
301
  for col in range(5):
302
  idx = col * 2
303
  if idx + 1 < 13:
304
  self._configure_cell(note_table.cell(1, idx), "NEG", italic=True, font_size=recap_font_size, alignment=WD_ALIGN_PARAGRAPH.CENTER)
305
  self._configure_cell(note_table.cell(1, idx + 1), "Note", italic=True, font_size=recap_font_size, alignment=WD_ALIGN_PARAGRAPH.CENTER)
306
  for col in range(10, 13):
307
- if col < 13:
308
- self._configure_cell(note_table.cell(1, col), "Note", italic=True, font_size=recap_font_size, alignment=WD_ALIGN_PARAGRAPH.CENTER)
309
-
310
- # Row 3 is left empty for scores
311
-
312
  self.document.add_paragraph().paragraph_format.space_after = Pt(6 * self.spacing_factor)
313
-
314
- except Exception as e:
315
- print(f"Error creating recap table: {e}")
316
- traceback.print_exc()
317
-
318
 
319
  def ajouter_note_candidat_avec_cadre(self):
320
- """Adds the NB2 instructions in a green box."""
321
  try:
322
  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
323
- cell = note_table.cell(0, 0); self._set_cell_shading(cell, "C6E0B4") # Light Green Shading
324
  p = cell.paragraphs[0]; p.paragraph_format.space_before = Pt(2); p.paragraph_format.space_after = Pt(2)
325
- font_size = max(7 * self.spacing_factor, 6) # Min 6pt for readability
326
  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).")
327
  run.italic = True; run.font.size = Pt(font_size)
328
  self.document.add_paragraph().paragraph_format.space_after = Pt(8 * self.spacing_factor)
329
- except Exception as e:
330
- print(f"Error adding candidate note box: {e}")
331
-
332
 
333
  def ajouter_zone_note(self):
334
- """Adds the 'Note finale/20' label and the empty score box."""
335
  try:
336
- para_note_label = self.document.add_paragraph(); para_note_label.alignment = WD_ALIGN_PARAGRAPH.RIGHT
337
- para_note_label.paragraph_format.space_after = Pt(1); para_note_label.paragraph_format.space_before = Pt(4 * self.spacing_factor)
338
  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)
339
-
340
  box_table = self.document.add_table(rows=1, cols=1); box_table.style = 'Table Grid'; box_table.alignment = WD_TABLE_ALIGNMENT.RIGHT
341
  box_size = Cm(1.5); cell = box_table.cell(0, 0); cell.width = box_size; box_table.rows[0].height = box_size
342
- self._configure_cell(cell, "", v_alignment=WD_ALIGN_VERTICAL.CENTER) # Ensure empty and centered
343
- # Optional shading for the box: self._set_cell_shading(cell, "F2F2F2") # Light Gray
344
-
345
  self.document.add_paragraph().paragraph_format.space_after = Pt(8 * self.spacing_factor)
346
- except Exception as e:
347
- print(f"Error adding final score zone: {e}")
348
-
349
 
350
  def ajouter_lignes_correcteurs(self):
351
- """Adds lines for corrector signatures."""
352
  try:
353
  num_elements = len(self.elements_techniques); use_compact_mode = num_elements > 12
354
  if use_compact_mode:
@@ -361,9 +217,7 @@ class EvaluationGymnique:
361
  chars_per_cm_estimate = 3; line_length_cm = 10; points_count = int(line_length_cm * chars_per_cm_estimate)
362
  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)
363
  para.add_run("." * points_count).font.size = Pt(self.dynamic_font_size)
364
- except Exception as e:
365
- print(f"Error adding corrector lines: {e}")
366
-
367
 
368
  def modifier_centre_examen(self, nom): self.centre_examen = nom
369
  def modifier_type_examen(self, type_examen): self.type_examen = type_examen
@@ -373,156 +227,111 @@ class EvaluationGymnique:
373
  def modifier_candidat(self, nom): self.nom_candidat = nom
374
 
375
  def ajouter_element(self, nom, categorie, points):
376
- try: point_value = float(str(points).replace(',', '.')) # Ensure conversion from string, handle comma
377
  except (ValueError, TypeError): print(f"Warning: Invalid points value '{points}' for element '{nom}'. Using 0.0."); point_value = 0.0
378
  self.elements_techniques.append({"nom": nom, "categorie": categorie, "points": point_value})
379
 
380
  def generer_document(self, nom_fichier="evaluation_gymnastique.docx"):
381
- """Generates the complete Word document."""
382
  try:
383
- self.calculate_dynamic_sizing()
384
- self.ajouter_entete_colore()
385
- self.creer_tableau_elements() # Includes single newline after name
386
- self.ajouter_note_jury()
387
- self.creer_tableau_recapitulatif()
388
- self.ajouter_lignes_correcteurs() # Before final score box usually
389
- self.ajouter_zone_note()
390
- self.ajouter_note_candidat_avec_cadre() # Keep green box near the end
391
-
392
- self.document.save(nom_fichier)
393
- print(f"Document '{nom_fichier}' generated successfully.")
394
  return nom_fichier
395
- except Exception as e:
396
- print(f"FATAL error generating document: {e}")
397
- traceback.print_exc()
398
- return None # Indicate failure
399
 
400
  # --- Flask Application ---
401
  app = Flask(__name__)
402
  app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
403
- app.secret_key = os.urandom(24) # Needed for flash messages
404
 
405
  @app.route("/eps", methods=["GET", "POST"])
406
  def index():
407
  if request.method == "POST":
408
- docx_filepath = None # Initialize variable
409
- pdf_filepath = None # Initialize variable
410
  try:
411
- # --- Récupération des informations ---
412
- centre_examen = request.form.get("centre_examen", "Centre d'examen")
413
- type_examen = request.form.get("type_examen", "Bac Général")
414
- serie = request.form.get("serie", "Série")
415
- etablissement = request.form.get("etablissement", "Établissement")
416
- session_value = request.form.get("session", "2025")
417
- nom_candidat = request.form.get("nom_candidat", "Candidat")
418
  output_format = request.form.get("format", "docx")
419
 
420
- # --- Création et configuration du document ---
421
- evaluation = EvaluationGymnique()
422
- evaluation.modifier_centre_examen(centre_examen); evaluation.modifier_type_examen(type_examen)
423
- evaluation.modifier_serie(serie); evaluation.modifier_etablissement(etablissement)
424
- evaluation.modifier_session(session_value); evaluation.modifier_candidat(nom_candidat)
425
-
426
- # --- Récupération des éléments techniques ---
427
- element_names = request.form.getlist("new_element_name")
428
- element_categories = request.form.getlist("new_element_categorie")
429
- element_points = request.form.getlist("new_element_points")
430
 
 
 
431
  num_elements_added = 0
432
  for name, cat, pts in zip(element_names, element_categories, element_points):
433
  if name and name.strip() and cat and cat.strip() and pts and pts.strip():
434
- evaluation.ajouter_element(name.strip(), cat.strip(), pts.strip())
435
- num_elements_added += 1
436
- else:
437
- # Optionally log skipped incomplete elements for debugging
438
- # print(f"Skipping incomplete element: Name='{name}', Cat='{cat}', Pts='{pts}'")
439
- pass # Silently skip incomplete rows
440
-
441
- if num_elements_added == 0:
442
- flash("Aucun élément technique complet n'a été fourni. Le document sera généré sans éléments.", "warning")
443
- # Allow generation even with no elements, or redirect back:
444
- # return redirect(url_for('index'))
445
 
446
- # --- Génération du document DOCX ---
447
- safe_candidat_name = "".join(c if c.isalnum() else "_" for c in nom_candidat) # Sanitize name for filename
448
- safe_session = "".join(c if c.isalnum() else "_" for c in session_value)
449
- base_filename = f"evaluation_{safe_candidat_name}_{safe_session}"
450
- docx_filename = f"{base_filename}.docx"
451
  docx_filepath = os.path.join(app.config['UPLOAD_FOLDER'], docx_filename)
452
-
453
  generated_docx = evaluation.generer_document(docx_filepath)
 
454
 
455
- if not generated_docx: # Check if generation failed
456
- flash("Une erreur est survenue lors de la génération du document DOCX.", "error")
457
- return redirect(url_for('index'))
458
-
459
- # --- Conversion et envoi ---
460
  if output_format == "pdf":
461
- print(f"Attempting PDF conversion for {docx_filepath}...")
462
  try:
463
- # Check API key again before conversion attempt
464
- if not convertapi.api_credentials:
465
  flash("La clé API pour ConvertAPI n'est pas configurée correctement. Impossible de générer le PDF.", "error")
466
- # Optionally send the docx as fallback?
467
- # return send_file(docx_filepath, as_attachment=True, download_name=docx_filename)
468
- return redirect(url_for('index'))
469
 
 
470
  result = convertapi.convert('pdf', { 'File': docx_filepath }, from_format = 'docx')
471
- pdf_filename_base = f"{base_filename}.pdf"
472
- pdf_filepath = os.path.join(app.config['UPLOAD_FOLDER'], pdf_filename_base)
473
- result.save_files(pdf_filepath)
474
- print(f"PDF saved to {pdf_filepath}")
475
- # Send the generated PDF
476
  return send_file(pdf_filepath, as_attachment=True, download_name=pdf_filename_base)
477
 
478
  except convertapi.ApiError as api_err:
479
- print(f"ConvertAPI Error: {api_err}")
480
  flash(f"Erreur ConvertAPI: {api_err}. Vérifiez votre clé ou vos crédits. Le fichier DOCX sera téléchargé.", "warning")
481
- # Fallback to sending DOCX
482
- return send_file(docx_filepath, as_attachment=True, download_name=docx_filename)
483
  except Exception as e:
484
- print(f"Error during PDF conversion: {e}")
485
- traceback.print_exc()
486
  flash(f"Erreur lors de la conversion PDF: {e}. Le fichier DOCX sera téléchargé.", "warning")
487
- # Fallback to sending DOCX
488
- return send_file(docx_filepath, as_attachment=True, download_name=docx_filename)
489
 
490
- else: # Send the generated DOCX
 
491
  return send_file(docx_filepath, as_attachment=True, download_name=docx_filename)
492
 
493
  except Exception as e:
494
- print(f"An error occurred during POST request processing: {e}")
495
- traceback.print_exc()
496
- flash(f"Une erreur interne est survenue: {e}", "error")
497
- return redirect(url_for('index')) # Redirect back to form on error
498
 
 
 
 
499
  finally:
500
- # Clean up temporary files (optional, consider delay or cron job for robustness)
501
- # Be careful cleaning up docx if PDF failed and you sent docx as fallback
502
- if output_format == "pdf" and pdf_filepath and os.path.exists(pdf_filepath):
503
- # If PDF was successfully sent (or attempted), remove the source docx
504
- if docx_filepath and os.path.exists(docx_filepath):
505
- try: os.remove(docx_filepath)
506
- except OSError as e: print(f"Error removing temp docx: {e}")
507
- # Maybe remove the PDF too after sending? Or keep it for a while?
508
- # try: os.remove(pdf_filepath)
509
- # except OSError as e: print(f"Error removing temp pdf: {e}")
510
- elif output_format == "docx" and docx_filepath and os.path.exists(docx_filepath):
511
- # Maybe remove the DOCX after sending? Or keep it? Let's keep it for now.
512
- pass
513
-
514
-
515
- # --- Affichage du formulaire (GET request) ---
516
  return render_template("index.html")
517
 
518
  if __name__ == "__main__":
519
- # Make sure the UPLOAD_FOLDER exists when running directly
520
  if not os.path.exists(UPLOAD_FOLDER):
521
- try:
522
- os.makedirs(UPLOAD_FOLDER)
523
- except OSError as e:
524
- print(f"CRITICAL: Could not create upload folder '{UPLOAD_FOLDER}'. Exiting. Error: {e}")
525
- exit() # Exit if we can't create the folder
526
-
527
- # Consider security implications of debug=True in production
528
- app.run(debug=True, host='0.0.0.0', port=5001) # Run on port 5001, accessible on network
 
1
+ # --- START OF COMPLETE FLASK APP SCRIPT (v6 - Credentials Fix) ---
2
  from flask import Flask, render_template, request, send_file, flash, redirect, url_for
3
  import os
4
  import convertapi # For PDF conversion
 
12
  import traceback # For detailed error logging
13
 
14
  # --- Configuration ---
15
+ # **** CORRECTED CONFIGURATION VARIABLE ****
 
16
  convertapi.api_credentials = 'secret_8wCI6pgOP9AxLVJG'
17
+
18
  # Define a temporary directory for generated files
19
  UPLOAD_FOLDER = 'temp_files'
20
  if not os.path.exists(UPLOAD_FOLDER):
 
22
  os.makedirs(UPLOAD_FOLDER)
23
  except OSError as e:
24
  print(f"Error creating directory {UPLOAD_FOLDER}: {e}")
 
25
 
26
 
27
+ # --- Classe de génération de document (v5 logic - unchanged) ---
28
  class EvaluationGymnique:
29
  def __init__(self):
30
  self.document = Document()
 
31
  try:
32
  section = self.document.sections[0]
33
+ section.page_height = Cm(29.7); section.page_width = Cm(21)
34
+ section.left_margin = Cm(1.5); section.right_margin = Cm(1.5)
35
+ section.top_margin = Cm(1); section.bottom_margin = Cm(1)
36
+ except Exception as e: print(f"Error setting up document sections: {e}")
37
+ self.centre_examen = "Centre d'examen"; self.type_examen = "Bac Général"; self.serie = "Série"
38
+ self.etablissement = "Établissement"; self.session = "2025"; self.nom_candidat = "Candidat"
 
 
 
 
 
 
 
 
 
 
 
39
  self.elements_techniques = []
40
+ self.base_font_size = 10; self.base_header_font_size = 14; self.base_row_height = 1.1; self.table_font_size = 9
41
+ self.available_height = 27.7; self.fixed_elements_height = 15
42
+ self.dynamic_font_size = self.base_font_size; self.dynamic_header_font_size = self.base_header_font_size
43
+ self.dynamic_table_font_size = self.table_font_size; self.dynamic_row_height = self.base_row_height
 
 
 
 
 
 
 
 
 
 
 
44
  self.spacing_factor = 1.0
45
 
46
  def calculate_dynamic_sizing(self):
47
+ num_elements = len(self.elements_techniques); estimated_table_height = (num_elements + 1) * self.base_row_height * 1.3
 
 
 
 
48
  available_space_for_table = self.available_height - self.fixed_elements_height
49
+ if estimated_table_height > available_space_for_table and num_elements > 5:
50
+ reduction_factor = max(0.6, 1 - (max(0, num_elements - 5) * 0.04))
51
+ self.dynamic_font_size = max(self.base_font_size * reduction_factor, 7)
52
+ self.dynamic_header_font_size = max(self.base_header_font_size * reduction_factor, 10)
53
+ self.dynamic_table_font_size = max(self.table_font_size * reduction_factor, 7)
54
+ self.dynamic_row_height = max(self.base_row_height * (reduction_factor + 0.1), 0.8)
55
+ self.spacing_factor = max(reduction_factor, 0.4)
 
 
 
 
56
  else:
57
+ self.dynamic_font_size = self.base_font_size; self.dynamic_header_font_size = self.base_header_font_size
58
+ self.dynamic_table_font_size = self.table_font_size; self.dynamic_row_height = self.base_row_height
 
 
 
59
  self.spacing_factor = 1.0
 
60
 
61
  def _set_cell_shading(self, cell, fill_color):
 
62
  try:
63
  shading_elm = parse_xml(f'<w:shd {nsdecls("w")} w:fill="{fill_color}"/>')
64
  cell._tc.get_or_add_tcPr().append(shading_elm)
65
+ except Exception as e: print(f"Error setting cell shading: {e}")
 
66
 
67
+ 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):
68
+ cell.text = text; cell.vertical_alignment = v_alignment
69
+ paragraph = cell.paragraphs[0]; paragraph.alignment = alignment
70
+ paragraph.paragraph_format.space_before = Pt(0); paragraph.paragraph_format.space_after = Pt(0)
 
 
 
 
 
 
71
  if paragraph.runs:
72
+ run = paragraph.runs[0]; run.bold = bold; run.italic = italic
73
+ if font_size: run.font.size = Pt(font_size)
74
+ if color_rgb: run.font.color.rgb = color_rgb
75
+ return paragraph
 
 
 
 
76
 
77
  def ajouter_entete_colore(self):
 
78
  try:
79
+ header_paragraph = self.document.add_paragraph(); header_paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER; header_paragraph.space_after = Pt(6 * self.spacing_factor)
80
+ 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)
81
+ header_table = self.document.add_table(rows=3, cols=2); header_table.style = 'Table Grid'; header_table.autofit = False
 
 
 
 
 
 
 
 
82
  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
83
  available_table_width = page_width_cm - left_margin_cm - right_margin_cm
84
  col_widths = [available_table_width * 0.55, available_table_width * 0.45]
85
  for i, width in enumerate(col_widths):
86
  for cell in header_table.columns[i].cells: cell.width = Cm(width)
 
87
  row_height_cm = max(0.6, 0.8 * self.spacing_factor)
88
  for row in header_table.rows:
89
  row.height = Cm(row_height_cm)
90
  for cell in row.cells:
91
+ cell.vertical_alignment = WD_ALIGN_VERTICAL.CENTER; self._set_cell_shading(cell, "D9E2F3")
 
92
  for p in cell.paragraphs: p.paragraph_format.space_before = Pt(0); p.paragraph_format.space_after = Pt(0)
93
+ 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)]]
94
+ text_color = RGBColor(0, 32, 96)
 
 
 
 
 
 
95
  for r, row_data in enumerate(header_info):
96
  for c, (label, value) in enumerate(row_data):
97
+ cell = header_table.cell(r, c); p = cell.paragraphs[0]; p.clear()
 
98
  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
99
  run_value = p.add_run(value); run_value.font.size = Pt(self.dynamic_font_size)
 
100
  self.document.add_paragraph().paragraph_format.space_after = Pt(4 * self.spacing_factor)
101
+ except Exception as e: print(f"Error adding header: {e}"); traceback.print_exc()
 
 
 
 
102
 
103
  def creer_tableau_elements(self):
 
104
  try:
105
+ num_elements = len(self.elements_techniques);
106
+ if num_elements == 0: return
107
+ table = self.document.add_table(rows=num_elements + 1, cols=5); table.style = 'Table Grid'; table.alignment = WD_TABLE_ALIGNMENT.CENTER; table.autofit = False
 
 
 
108
  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
109
  available_table_width = page_width_cm - left_margin_cm - right_margin_cm
110
  total_prop = 8 + 3 + 2 + 2.5 + 2.5
111
  col_widths_cm = [available_table_width * (p / total_prop) for p in [8, 3, 2, 2.5, 2.5]]
112
  for i, width in enumerate(col_widths_cm):
113
  for cell in table.columns[i].cells: cell.width = Cm(width)
 
114
  min_row_height_cm = max(0.8, self.dynamic_row_height)
115
  for row in table.rows:
116
  row.height_rule = WD_ROW_HEIGHT_RULE.AT_LEAST; row.height = Cm(min_row_height_cm)
117
  for cell in row.cells:
118
  cell.vertical_alignment = WD_ALIGN_VERTICAL.CENTER
119
  for p in cell.paragraphs: p.paragraph_format.space_before = Pt(0); p.paragraph_format.space_after = Pt(0)
120
+ header_row = table.rows[0]; header_color = RGBColor(0, 32, 96)
121
+ headers_config = [("ELEMENTS TECHNIQUES", {"bold": True, "font_size": self.dynamic_table_font_size, "color_rgb": header_color, "alignment": WD_ALIGN_PARAGRAPH.CENTER}), ("CATEGORIES D'ELEMENTS TECHNIQUES ET PONDERATION", {"bold": True, "font_size": self.dynamic_table_font_size, "color_rgb": header_color, "alignment": WD_ALIGN_PARAGRAPH.CENTER}), ("", {}), ("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})]
 
 
 
 
 
 
 
 
122
  for i, (text, config) in enumerate(headers_config):
123
+ cell = header_row.cells[i]; self._set_cell_shading(cell, "BDD7EE")
124
+ if i != 2: self._configure_cell(cell, text, **config)
 
 
 
125
  try: table.cell(0, 1).merge(table.cell(0, 2))
126
  except Exception as merge_err: print(f"Error merging header cells: {merge_err}")
 
 
127
  for i, element in enumerate(self.elements_techniques, 1):
128
+ if i >= len(table.rows): continue
129
+ self._configure_cell(table.cell(i, 0), f'{element["nom"]}\n', font_size=self.dynamic_table_font_size, alignment=WD_ALIGN_PARAGRAPH.LEFT, v_alignment=WD_ALIGN_VERTICAL.CENTER)
130
+ 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)
131
+ 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)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
132
  self._configure_cell(table.cell(i, 3), "", font_size=self.dynamic_table_font_size)
 
133
  self._configure_cell(table.cell(i, 4), "", font_size=self.dynamic_table_font_size)
 
134
  self.document.add_paragraph().paragraph_format.space_after = Pt(6 * self.spacing_factor)
135
+ except Exception as e: print(f"Error creating elements table: {e}"); traceback.print_exc()
 
 
 
 
136
 
137
  def ajouter_note_jury(self):
 
138
  try:
139
  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)
140
  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.")
141
+ run.bold = True; run.font.color.rgb = RGBColor(255, 0, 0); run.font.size = Pt(max(self.dynamic_font_size - 2, 6))
142
+ except Exception as e: print(f"Error adding jury note: {e}")
 
 
143
 
144
  def creer_tableau_recapitulatif(self):
 
145
  try:
146
  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
 
147
  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
148
  available_recap_width = page_width_cm - left_margin_cm - right_margin_cm
149
  width_A_E_pair = available_recap_width * (1.2 / 13.0); width_final_single = available_recap_width * (1.0 / 13.0)
 
154
  for i, width in enumerate(col_widths_recap):
155
  adjusted_width = max(0.5, width + width_adjustment);
156
  for cell in note_table.columns[i].cells: cell.width = Cm(adjusted_width)
 
157
  row_height_cm = max(0.5, 0.6 * self.spacing_factor)
158
  for row in note_table.rows:
159
  row.height = Cm(row_height_cm)
160
  for cell in row.cells: cell.vertical_alignment = WD_ALIGN_VERTICAL.CENTER;
161
  for p in cell.paragraphs: p.paragraph_format.space_before = Pt(0); p.paragraph_format.space_after = Pt(0)
162
+ header_color = RGBColor(0, 32, 96); header_fill = "BDD7EE"; recap_font_size = max(self.dynamic_table_font_size - 1, 6)
 
 
 
 
163
  for cell in note_table.rows[0].cells: self._set_cell_shading(cell, header_fill)
 
164
  type_data = [("A", "1pt"), ("B", "1,5pt"), ("C", "2pts"), ("D", "2,5pts"), ("E", "3pts")]
165
  for col, (type_lettre, points) in enumerate(type_data):
166
  idx = col * 2
167
  if idx + 1 < 13:
168
  cell = note_table.cell(0, idx)
169
  try:
170
+ 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)
 
171
  except Exception as e: print(f"Error merging recap cells at index {idx}: {e}")
 
172
  final_headers = [("ROV", "2pts"), ("Projet", "2pts"), ("Réalisation", "16pts")]
173
  for col_offset, (titre, points) in enumerate(final_headers):
174
  col = 10 + col_offset
175
+ 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)
 
 
 
176
  for col in range(5):
177
  idx = col * 2
178
  if idx + 1 < 13:
179
  self._configure_cell(note_table.cell(1, idx), "NEG", italic=True, font_size=recap_font_size, alignment=WD_ALIGN_PARAGRAPH.CENTER)
180
  self._configure_cell(note_table.cell(1, idx + 1), "Note", italic=True, font_size=recap_font_size, alignment=WD_ALIGN_PARAGRAPH.CENTER)
181
  for col in range(10, 13):
182
+ if col < 13: self._configure_cell(note_table.cell(1, col), "Note", italic=True, font_size=recap_font_size, alignment=WD_ALIGN_PARAGRAPH.CENTER)
 
 
 
 
183
  self.document.add_paragraph().paragraph_format.space_after = Pt(6 * self.spacing_factor)
184
+ except Exception as e: print(f"Error creating recap table: {e}"); traceback.print_exc()
 
 
 
 
185
 
186
  def ajouter_note_candidat_avec_cadre(self):
 
187
  try:
188
  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
189
+ cell = note_table.cell(0, 0); self._set_cell_shading(cell, "C6E0B4")
190
  p = cell.paragraphs[0]; p.paragraph_format.space_before = Pt(2); p.paragraph_format.space_after = Pt(2)
191
+ font_size = max(7 * self.spacing_factor, 6)
192
  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).")
193
  run.italic = True; run.font.size = Pt(font_size)
194
  self.document.add_paragraph().paragraph_format.space_after = Pt(8 * self.spacing_factor)
195
+ except Exception as e: print(f"Error adding candidate note box: {e}")
 
 
196
 
197
  def ajouter_zone_note(self):
 
198
  try:
199
+ 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)
 
200
  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)
 
201
  box_table = self.document.add_table(rows=1, cols=1); box_table.style = 'Table Grid'; box_table.alignment = WD_TABLE_ALIGNMENT.RIGHT
202
  box_size = Cm(1.5); cell = box_table.cell(0, 0); cell.width = box_size; box_table.rows[0].height = box_size
203
+ self._configure_cell(cell, "", v_alignment=WD_ALIGN_VERTICAL.CENTER)
 
 
204
  self.document.add_paragraph().paragraph_format.space_after = Pt(8 * self.spacing_factor)
205
+ except Exception as e: print(f"Error adding final score zone: {e}")
 
 
206
 
207
  def ajouter_lignes_correcteurs(self):
 
208
  try:
209
  num_elements = len(self.elements_techniques); use_compact_mode = num_elements > 12
210
  if use_compact_mode:
 
217
  chars_per_cm_estimate = 3; line_length_cm = 10; points_count = int(line_length_cm * chars_per_cm_estimate)
218
  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)
219
  para.add_run("." * points_count).font.size = Pt(self.dynamic_font_size)
220
+ except Exception as e: print(f"Error adding corrector lines: {e}")
 
 
221
 
222
  def modifier_centre_examen(self, nom): self.centre_examen = nom
223
  def modifier_type_examen(self, type_examen): self.type_examen = type_examen
 
227
  def modifier_candidat(self, nom): self.nom_candidat = nom
228
 
229
  def ajouter_element(self, nom, categorie, points):
230
+ try: point_value = float(str(points).replace(',', '.'))
231
  except (ValueError, TypeError): print(f"Warning: Invalid points value '{points}' for element '{nom}'. Using 0.0."); point_value = 0.0
232
  self.elements_techniques.append({"nom": nom, "categorie": categorie, "points": point_value})
233
 
234
  def generer_document(self, nom_fichier="evaluation_gymnastique.docx"):
 
235
  try:
236
+ self.calculate_dynamic_sizing(); self.ajouter_entete_colore(); self.creer_tableau_elements(); self.ajouter_note_jury(); self.creer_tableau_recapitulatif(); self.ajouter_lignes_correcteurs(); self.ajouter_zone_note(); self.ajouter_note_candidat_avec_cadre()
237
+ self.document.save(nom_fichier); print(f"Document '{nom_fichier}' generated successfully.")
 
 
 
 
 
 
 
 
 
238
  return nom_fichier
239
+ except Exception as e: print(f"FATAL error generating document: {e}"); traceback.print_exc(); return None
 
 
 
240
 
241
  # --- Flask Application ---
242
  app = Flask(__name__)
243
  app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
244
+ app.secret_key = os.urandom(24)
245
 
246
  @app.route("/eps", methods=["GET", "POST"])
247
  def index():
248
  if request.method == "POST":
249
+ docx_filepath = None; pdf_filepath = None
 
250
  try:
251
+ # --- Get Form Data ---
252
+ centre_examen = request.form.get("centre_examen", "Centre d'examen"); type_examen = request.form.get("type_examen", "Bac Général")
253
+ serie = request.form.get("serie", "Série"); etablissement = request.form.get("etablissement", "Établissement")
254
+ session_value = request.form.get("session", "2025"); nom_candidat = request.form.get("nom_candidat", "Candidat")
 
 
 
255
  output_format = request.form.get("format", "docx")
256
 
257
+ # --- Create Document Instance ---
258
+ evaluation = EvaluationGymnique(); evaluation.modifier_centre_examen(centre_examen); evaluation.modifier_type_examen(type_examen)
259
+ evaluation.modifier_serie(serie); evaluation.modifier_etablissement(etablissement); evaluation.modifier_session(session_value); evaluation.modifier_candidat(nom_candidat)
 
 
 
 
 
 
 
260
 
261
+ # --- Get Elements ---
262
+ element_names = request.form.getlist("new_element_name"); element_categories = request.form.getlist("new_element_categorie"); element_points = request.form.getlist("new_element_points")
263
  num_elements_added = 0
264
  for name, cat, pts in zip(element_names, element_categories, element_points):
265
  if name and name.strip() and cat and cat.strip() and pts and pts.strip():
266
+ evaluation.ajouter_element(name.strip(), cat.strip(), pts.strip()); num_elements_added += 1
267
+ 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")
 
 
 
 
 
 
 
 
 
268
 
269
+ # --- Generate DOCX ---
270
+ safe_candidat_name = "".join(c if c.isalnum() else "_" for c in nom_candidat); safe_session = "".join(c if c.isalnum() else "_" for c in session_value)
271
+ base_filename = f"evaluation_{safe_candidat_name}_{safe_session}"; docx_filename = f"{base_filename}.docx"
 
 
272
  docx_filepath = os.path.join(app.config['UPLOAD_FOLDER'], docx_filename)
 
273
  generated_docx = evaluation.generer_document(docx_filepath)
274
+ if not generated_docx: flash("Une erreur est survenue lors de la génération du document DOCX.", "error"); return redirect(url_for('index'))
275
 
276
+ # --- Convert / Send ---
 
 
 
 
277
  if output_format == "pdf":
278
+ print(f"Output format requested: PDF. Attempting conversion for {docx_filepath}...") # Debug print
279
  try:
280
+ # **** Check credentials before calling convert ****
281
+ if not convertapi.api_credentials or convertapi.api_credentials == 'YOUR_SECRET': # Check actual variable used
282
  flash("La clé API pour ConvertAPI n'est pas configurée correctement. Impossible de générer le PDF.", "error")
283
+ # Sending DOCX as fallback because API key is missing
284
+ print("ConvertAPI credentials missing, sending DOCX instead.")
285
+ return send_file(docx_filepath, as_attachment=True, download_name=docx_filename)
286
 
287
+ print(f"Credentials seem OK ('{convertapi.api_credentials[:5]}...'). Calling convertapi.convert...") # Debug print, hide most of key
288
  result = convertapi.convert('pdf', { 'File': docx_filepath }, from_format = 'docx')
289
+ pdf_filename_base = f"{base_filename}.pdf"; pdf_filepath = os.path.join(app.config['UPLOAD_FOLDER'], pdf_filename_base)
290
+ result.save_files(pdf_filepath); print(f"PDF saved to {pdf_filepath}")
 
 
 
291
  return send_file(pdf_filepath, as_attachment=True, download_name=pdf_filename_base)
292
 
293
  except convertapi.ApiError as api_err:
294
+ print(f"ConvertAPI Error during conversion: {api_err}")
295
  flash(f"Erreur ConvertAPI: {api_err}. Vérifiez votre clé ou vos crédits. Le fichier DOCX sera téléchargé.", "warning")
296
+ return send_file(docx_filepath, as_attachment=True, download_name=docx_filename) # Fallback
 
297
  except Exception as e:
298
+ print(f"Non-API Error during PDF conversion process: {e}"); traceback.print_exc()
 
299
  flash(f"Erreur lors de la conversion PDF: {e}. Le fichier DOCX sera téléchargé.", "warning")
300
+ return send_file(docx_filepath, as_attachment=True, download_name=docx_filename) # Fallback
 
301
 
302
+ else: # Send DOCX
303
+ print(f"Output format requested: DOCX. Sending {docx_filepath}...") # Debug print
304
  return send_file(docx_filepath, as_attachment=True, download_name=docx_filename)
305
 
306
  except Exception as e:
307
+ print(f"An error occurred during POST request processing: {e}"); traceback.print_exc()
308
+ flash(f"Une erreur interne est survenue: {e}", "error"); return redirect(url_for('index'))
 
 
309
 
310
+ # --- Cleanup (Consider moving to a background task) ---
311
+ # This basic cleanup might remove files before they are fully sent or if fallback occurs.
312
+ # Advanced cleanup is recommended for production.
313
  finally:
314
+ if pdf_filepath and os.path.exists(pdf_filepath):
315
+ # If PDF was created, definitely remove the source DOCX now
316
+ if docx_filepath and os.path.exists(docx_filepath):
317
+ try: os.remove(docx_filepath); print(f"Removed temp file: {docx_filepath}")
318
+ except OSError as e: print(f"Error removing temp docx: {e}")
319
+ # Optionally remove PDF after sending?
320
+ # try: os.remove(pdf_filepath); print(f"Removed temp file: {pdf_filepath}")
321
+ # except OSError as e: print(f"Error removing temp pdf: {e}")
322
+ elif docx_filepath and os.path.exists(docx_filepath) and output_format == 'docx':
323
+ # If only DOCX was generated and sent, maybe remove it?
324
+ # try: os.remove(docx_filepath); print(f"Removed temp file: {docx_filepath}")
325
+ # except OSError as e: print(f"Error removing temp docx: {e}")
326
+ pass # Keeping DOCX for now after sending
327
+
328
+
329
+ # --- GET Request ---
330
  return render_template("index.html")
331
 
332
  if __name__ == "__main__":
 
333
  if not os.path.exists(UPLOAD_FOLDER):
334
+ try: os.makedirs(UPLOAD_FOLDER)
335
+ except OSError as e: print(f"CRITICAL: Could not create upload folder '{UPLOAD_FOLDER}'. Exiting. Error: {e}"); exit()
336
+ # Changed port back to 5000 for consistency if preferred
337
+ app.run(debug=True, host='0.0.0.0', port=5000)