Docfile commited on
Commit
6cf0508
·
verified ·
1 Parent(s): e85cb9c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +388 -282
app.py CHANGED
@@ -1,4 +1,4 @@
1
- # --- START OF FLASK APP SCRIPT (v5 - Final) ---
2
  from flask import Flask, render_template, request, send_file, flash, redirect, url_for
3
  import os
4
  import convertapi # For PDF conversion
@@ -9,33 +9,40 @@ from docx.enum.table import WD_TABLE_ALIGNMENT, WD_ALIGN_VERTICAL, WD_ROW_HEIGHT
9
  from docx.oxml.ns import nsdecls
10
  from docx.oxml import parse_xml
11
  import math
12
- import uuid # For unique filenames
13
 
14
  # --- Configuration ---
15
- # IMPORTANT: Replace 'YOUR_SECRET' with your actual ConvertAPI secret
16
- # You can get one from https://www.convertapi.com/a
17
  convertapi.api_secret = 'secret_8wCI6pgOP9AxLVJG'
18
 
19
  # Define a temporary directory for generated files
20
- BASE_DIR = os.path.abspath(os.path.dirname(__file__))
21
- UPLOAD_FOLDER = os.path.join(BASE_DIR, 'temp_files')
22
  if not os.path.exists(UPLOAD_FOLDER):
23
- os.makedirs(UPLOAD_FOLDER)
 
 
 
 
24
 
25
 
26
- # --- Classe de génération de document (Modifiée pour interligne standard) ---
27
  class EvaluationGymnique:
28
  def __init__(self):
29
  self.document = Document()
30
- # --- Document Setup (Margins, etc.) ---
31
- self.document.sections[0].page_height = Cm(29.7)
32
- self.document.sections[0].page_width = Cm(21)
33
- self.document.sections[0].left_margin = Cm(1.5)
34
- self.document.sections[0].right_margin = Cm(1.5)
35
- self.document.sections[0].top_margin = Cm(1)
36
- self.document.sections[0].bottom_margin = Cm(1)
37
-
38
- # --- Default Data ---
 
 
 
 
 
39
  self.centre_examen = "Centre d'examen"
40
  self.type_examen = "Bac Général"
41
  self.serie = "Série"
@@ -43,18 +50,17 @@ class EvaluationGymnique:
43
  self.session = "2025"
44
  self.nom_candidat = "Candidat"
45
  self.elements_techniques = []
 
46
 
47
  # --- Layout Parameters ---
48
  self.base_font_size = 10
49
  self.base_header_font_size = 14
50
- # Adjusted base row height slightly for single line break comfort
51
- self.base_row_height = 1.0
52
  self.table_font_size = 9
53
- self.available_height = 27.7
54
- # Adjusted fixed height estimate slightly
55
- self.fixed_elements_height = 14
56
 
57
- # --- Dynamic Parameters (initialized) ---
58
  self.dynamic_font_size = self.base_font_size
59
  self.dynamic_header_font_size = self.base_header_font_size
60
  self.dynamic_table_font_size = self.table_font_size
@@ -62,221 +68,334 @@ class EvaluationGymnique:
62
  self.spacing_factor = 1.0
63
 
64
  def calculate_dynamic_sizing(self):
 
65
  num_elements = len(self.elements_techniques)
66
- # Estimate accounts for single line break now
67
- estimated_table_height = (num_elements + 1) * self.base_row_height * 1.3 # Reduced multiplier
68
 
69
  available_space_for_table = self.available_height - self.fixed_elements_height
70
- if estimated_table_height > available_space_for_table and num_elements > 0:
71
- reduction_factor = max(0.6, 1 - (max(0, num_elements - 10) * 0.04)) # Adjusted factor/threshold
72
- self.dynamic_font_size = max(self.base_font_size * reduction_factor, 6)
73
- self.dynamic_header_font_size = max(self.base_header_font_size * reduction_factor, 9)
74
- self.dynamic_table_font_size = max(self.table_font_size * reduction_factor, 6)
75
- self.dynamic_row_height = max(self.base_row_height * (reduction_factor + 0.1), 0.7) # Min 0.7cm
76
- self.spacing_factor = max(reduction_factor, 0.4)
77
- print(f"Adjusting sizes for {num_elements} elements. Factor: {reduction_factor:.2f}") # Optional debug
 
 
 
78
  else:
 
79
  self.dynamic_font_size = self.base_font_size
80
  self.dynamic_header_font_size = self.base_header_font_size
81
  self.dynamic_table_font_size = self.table_font_size
82
  self.dynamic_row_height = self.base_row_height
83
  self.spacing_factor = 1.0
84
- print(f"Using base sizes for {num_elements} elements.") # Optional debug
85
-
86
- def set_paragraph_format(self, paragraph, size=None, bold=False, italic=False, color_rgb=None, align=None, space_before=0, space_after=0):
87
- """Helper to format paragraphs and their first run."""
88
- paragraph.paragraph_format.space_before = Pt(space_before)
89
- paragraph.paragraph_format.space_after = Pt(space_after)
90
- if align is not None:
91
- paragraph.alignment = align
 
 
 
 
 
 
 
 
 
 
 
 
92
  if paragraph.runs:
93
  run = paragraph.runs[0]
94
- if size: run.font.size = Pt(size)
95
  run.bold = bold
96
  run.italic = italic
97
- if color_rgb: run.font.color.rgb = color_rgb
98
- # Consider adding a run if none exists, though setting text usually creates one.
 
 
 
99
 
100
  def ajouter_entete_colore(self):
101
- p = self.document.add_paragraph(); p.add_run("ÉVALUATION GYMNASTIQUE")
102
- 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)
103
- header_table = self.document.add_table(rows=3, cols=2); header_table.style = 'Table Grid'; header_table.autofit = False
104
- 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
105
- available_table_width = page_width_cm - left_margin_cm - right_margin_cm; col_widths = [available_table_width * 0.55, available_table_width * 0.45]
106
- for i, width in enumerate(col_widths):
107
- for cell in header_table.columns[i].cells: cell.width = Cm(width)
108
- row_height_cm = max(0.6, 0.8 * self.spacing_factor);
109
- for row in header_table.rows: row.height = Cm(row_height_cm)
110
- shading_fill = "D9E2F3"
111
- for row in header_table.rows:
112
- for cell in row.cells:
113
- 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)
114
- for p in cell.paragraphs: self.set_paragraph_format(p) # Ensure 0 spacing inside cells
115
- # Fill header data using helper
116
- def fill_header_cell(cell, label, value):
117
- 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)
118
- 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)
119
- fill_header_cell(header_table.cell(1, 0), "Série: ", self.serie); fill_header_cell(header_table.cell(1, 1), "Établissement: ", self.etablissement)
120
- fill_header_cell(header_table.cell(2, 0), "Session: ", self.session); fill_header_cell(header_table.cell(2, 1), "Candidat: ", self.nom_candidat)
121
- self.document.add_paragraph().paragraph_format.space_after = Pt(4 * self.spacing_factor)
122
-
123
- # --- MODIFIED METHOD for Line Breaks ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
  def creer_tableau_elements(self):
125
- num_elements = len(self.elements_techniques);
126
- if num_elements == 0: return
127
- table = self.document.add_table(rows=num_elements + 1, cols=5); table.style = 'Table Grid'; table.alignment = WD_TABLE_ALIGNMENT.CENTER; table.autofit = False
128
- 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
129
- available_table_width = page_width_cm - left_margin_cm - right_margin_cm; total_prop = 8 + 3 + 2 + 2.5 + 2.5
130
- col_widths_cm = [available_table_width * (p / total_prop) for p in [8, 3, 2, 2.5, 2.5]]
131
- for i, width in enumerate(col_widths_cm):
132
- for cell in table.columns[i].cells: cell.width = Cm(width)
133
- min_row_height_cm = max(0.7, self.dynamic_row_height) # Adjusted min height
134
- for row in table.rows:
135
- row.height_rule = WD_ROW_HEIGHT_RULE.AT_LEAST; row.height = Cm(min_row_height_cm)
136
- for cell in row.cells:
137
- cell.vertical_alignment = WD_ALIGN_VERTICAL.CENTER
138
- # Ensure default paragraph spacing is 0 unless overridden
139
- for p in cell.paragraphs: self.set_paragraph_format(p)
140
- header_row = table.rows[0]
141
- shading_fill = "BDD7EE"
142
- 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)
143
- headers = ["ELEMENTS TECHNIQUES", "CATEGORIES D'ELEMENTS TECHNIQUES ET PONDERATION", "", "APPRECIATIONS", "POINTS Accordés"]
144
- for i, header in enumerate(headers):
145
- cell = table.cell(0, i); p = cell.paragraphs[0]; p.clear(); p.add_run(header)
146
- self.set_paragraph_format(p, size=self.dynamic_table_font_size, bold=True, color_rgb=RGBColor(0, 32, 96), align=WD_ALIGN_PARAGRAPH.CENTER)
147
- try: table.cell(0, 1).merge(table.cell(0, 2))
148
- except Exception as e: print(f"Error merging cells: {e}")
149
- for i, element in enumerate(self.elements_techniques, 1):
150
- if i >= len(table.rows): continue
151
- # --- Element Name Cell ---
152
- element_cell = table.cell(i, 0)
153
- # Set text with a single newline
154
- element_cell.text = f'{element["nom"]}\n'
155
- element_cell.vertical_alignment = WD_ALIGN_VERTICAL.TOP # Align text to top
156
- # Format the paragraph containing the name and newline
157
- if element_cell.paragraphs:
158
- 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
159
-
160
- # --- Other Cells ---
161
- def fill_element_cell(cell_index, text, align, is_bold=True, is_italic=True):
162
- cell = table.cell(i, cell_index); cell.text = str(text) # Ensure text is string
163
- if cell.paragraphs: self.set_paragraph_format(cell.paragraphs[0], size=self.dynamic_table_font_size, bold=is_bold, italic=is_italic, align=align)
164
- fill_element_cell(1, element["categorie"], WD_ALIGN_PARAGRAPH.CENTER)
165
- fill_element_cell(2, element["points"], WD_ALIGN_PARAGRAPH.CENTER)
166
- fill_element_cell(3, "", WD_ALIGN_PARAGRAPH.CENTER, is_bold=False, is_italic=False) # Appreciation
167
- fill_element_cell(4, "", WD_ALIGN_PARAGRAPH.CENTER, is_bold=False, is_italic=False) # Points Accordés
168
-
169
- self.document.add_paragraph().paragraph_format.space_after = Pt(6 * self.spacing_factor)
170
- # --- END OF MODIFIED METHOD ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
171
 
172
  def ajouter_note_jury(self):
173
- 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.")
174
- 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)
 
 
 
 
 
 
175
 
176
  def creer_tableau_recapitulatif(self):
177
- 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
178
- 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
179
- 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)
180
- col_widths_recap = [];
181
- for _ in range(5): col_widths_recap.extend([width_A_E_pair / 2, width_A_E_pair / 2])
182
- col_widths_recap.extend([width_final_single, width_final_single, width_final_single])
183
- current_total_width = sum(col_widths_recap); width_adjustment = (available_recap_width - current_total_width) / len(col_widths_recap)
184
- for i, width in enumerate(col_widths_recap):
185
- adjusted_width = max(0.5, width + width_adjustment);
186
- for cell in note_table.columns[i].cells: cell.width = Cm(adjusted_width)
187
- row_height_cm = max(0.5, 0.6 * self.spacing_factor)
188
- shading_fill = "BDD7EE"
189
- for row in note_table.rows:
190
- row.height = Cm(row_height_cm)
191
- for cell in row.cells: cell.vertical_alignment = WD_ALIGN_VERTICAL.CENTER;
192
- for p in cell.paragraphs: self.set_paragraph_format(p)
193
- 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)
194
- recap_font_size = max(self.dynamic_table_font_size - 1, 5)
195
- header_color = RGBColor(0, 32, 96)
196
- type_data = [("A", "1pt"), ("B", "1,5pt"), ("C", "2pts"), ("D", "2,5pts"), ("E", "3pts")]
197
- for col, (type_lettre, points) in enumerate(type_data):
198
- idx = col * 2
199
- if idx + 1 < len(note_table.columns):
200
- cell = note_table.cell(0, idx)
201
- try:
202
- cell.merge(note_table.cell(0, idx + 1)); p = cell.paragraphs[0]; p.clear(); p.add_run(f"Type {type_lettre}\n{points}")
203
- self.set_paragraph_format(p, size=recap_font_size, bold=True, color_rgb=header_color, align=WD_ALIGN_PARAGRAPH.CENTER)
204
- except Exception as e: print(f"Error merging cells at index {idx}: {e}") # Optional logging
205
- final_headers = [("ROV", "2pts"), ("Projet", "2pts"), ("Réalisation", "16pts")]
206
- for col_offset, (titre, points) in enumerate(final_headers):
207
- col = 10 + col_offset
208
- if col < len(note_table.columns):
209
- cell = note_table.cell(0, col); p = cell.paragraphs[0]; p.clear(); p.add_run(f"{titre}\n{points}")
210
- self.set_paragraph_format(p, size=recap_font_size, bold=True, color_rgb=header_color, align=WD_ALIGN_PARAGRAPH.CENTER)
211
- for col in range(5):
212
- idx = col * 2
213
- if idx + 1 < len(note_table.columns):
214
- 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)
215
- 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)
216
- for col in range(10, 13):
217
- if col < len(note_table.columns):
218
- 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)
219
- self.document.add_paragraph().paragraph_format.space_after = Pt(6 * self.spacing_factor)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
220
 
221
  def ajouter_note_candidat_avec_cadre(self):
222
- 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
223
- 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)
224
- p = cell.paragraphs[0]; p.clear()
225
- 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).")
226
- run.italic = True; run.font.size = Pt(max(6 * self.spacing_factor, 5))
227
- p.paragraph_format.space_before = Pt(2); p.paragraph_format.space_after = Pt(2)
228
- self.document.add_paragraph().paragraph_format.space_after = Pt(8 * self.spacing_factor)
 
 
 
 
 
229
 
230
  def ajouter_zone_note(self):
231
- p_label = self.document.add_paragraph(); p_label.add_run("Note finale/20")
232
- 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)
233
- box_table = self.document.add_table(rows=1, cols=1); box_table.style = 'Table Grid'; box_table.alignment = WD_TABLE_ALIGNMENT.RIGHT
234
- box_size = Cm(1.5); cell = box_table.cell(0, 0); cell.width = box_size; box_table.rows[0].height = box_size
235
- cell.vertical_alignment = WD_ALIGN_VERTICAL.CENTER; cell.text = ""; self.set_paragraph_format(cell.paragraphs[0])
236
- self.document.add_paragraph().paragraph_format.space_after = Pt(8 * self.spacing_factor)
 
 
 
 
 
 
 
 
 
237
 
238
  def ajouter_lignes_correcteurs(self):
239
- num_elements = len(self.elements_techniques); use_compact_mode = num_elements > 12
240
- if use_compact_mode:
241
- p = self.document.add_paragraph(); run = p.add_run("Correcteurs: "); run.bold = True; run.font.size = Pt(self.dynamic_font_size)
242
- p.add_run("Projet / Principal / ROV").font.size = Pt(self.dynamic_font_size); p.add_run("\n" + "." * 30)
243
- self.set_paragraph_format(p, space_before=4 * self.spacing_factor, space_after=4 * self.spacing_factor)
244
- else:
245
- for role in ["Projet", "Principal", "ROV"]:
246
- p = self.document.add_paragraph(); run = p.add_run(f"Correcteur {role} : "); run.bold = True; run.font.size = Pt(self.dynamic_font_size)
247
- 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)
248
- p.add_run("." * points_count).font.size = Pt(self.dynamic_font_size)
249
- self.set_paragraph_format(p, space_before=3 * self.spacing_factor, space_after=1 * self.spacing_factor)
 
 
 
 
 
250
 
251
- # --- Data Modifiers ---
252
  def modifier_centre_examen(self, nom): self.centre_examen = nom
253
  def modifier_type_examen(self, type_examen): self.type_examen = type_examen
254
  def modifier_serie(self, serie): self.serie = serie
255
  def modifier_etablissement(self, nom): self.etablissement = nom
256
  def modifier_session(self, annee): self.session = annee
257
  def modifier_candidat(self, nom): self.nom_candidat = nom
 
258
  def ajouter_element(self, nom, categorie, points):
259
- try: point_value = float(points)
260
  except (ValueError, TypeError): print(f"Warning: Invalid points value '{points}' for element '{nom}'. Using 0.0."); point_value = 0.0
261
  self.elements_techniques.append({"nom": nom, "categorie": categorie, "points": point_value})
262
 
263
  def generer_document(self, nom_fichier="evaluation_gymnastique.docx"):
264
- """Generates the complete DOCX document."""
265
- self.calculate_dynamic_sizing()
266
- self.ajouter_entete_colore()
267
- self.creer_tableau_elements()
268
- self.ajouter_note_jury()
269
- self.creer_tableau_recapitulatif()
270
- # Reordered slightly for common layout flow
271
- self.ajouter_note_candidat_avec_cadre() # Instructions often come before signatures/final score
272
- self.ajouter_lignes_correcteurs()
273
- self.ajouter_zone_note()
274
  try:
275
- self.document.save(nom_fichier); print(f"Document '{nom_fichier}' generated successfully.")
 
 
 
 
 
 
 
 
 
 
276
  return nom_fichier
277
  except Exception as e:
278
- print(f"Error saving document: {e}")
279
- raise # Re-raise the exception for Flask to handle
 
280
 
281
  # --- Flask Application ---
282
  app = Flask(__name__)
@@ -286,127 +405,111 @@ app.secret_key = os.urandom(24) # Needed for flash messages
286
  @app.route("/eps", methods=["GET", "POST"])
287
  def index():
288
  if request.method == "POST":
289
- docx_filepath = None # Initialize to None
290
- pdf_filepath = None # Initialize to None
291
  try:
292
- # --- Récupération des informations depuis le formulaire ---
293
  centre_examen = request.form.get("centre_examen", "Centre d'examen")
294
  type_examen = request.form.get("type_examen", "Bac Général")
295
  serie = request.form.get("serie", "Série")
296
  etablissement = request.form.get("etablissement", "Établissement")
297
  session_value = request.form.get("session", "2025")
298
- nom_candidat = request.form.get("nom_candidat", "Candidat").strip()
299
  output_format = request.form.get("format", "docx")
300
 
301
- # Basic validation for candidate name
302
- if not nom_candidat:
303
- flash("Le nom du candidat ne peut pas être vide.", "error")
304
- return redirect(url_for('index'))
305
-
306
  # --- Création et configuration du document ---
307
  evaluation = EvaluationGymnique()
308
- evaluation.modifier_centre_examen(centre_examen)
309
- evaluation.modifier_type_examen(type_examen)
310
- evaluation.modifier_serie(serie)
311
- evaluation.modifier_etablissement(etablissement)
312
- evaluation.modifier_session(session_value)
313
- evaluation.modifier_candidat(nom_candidat)
314
 
315
  # --- Récupération des éléments techniques ---
316
  element_names = request.form.getlist("new_element_name")
317
  element_categories = request.form.getlist("new_element_categorie")
318
  element_points = request.form.getlist("new_element_points")
319
 
320
- valid_elements_added = 0
321
  for name, cat, pts in zip(element_names, element_categories, element_points):
322
- if name.strip() and cat.strip() and pts.strip():
323
- try:
324
- # Validate points format (allow dot or comma, convert to float)
325
- pts_float = float(pts.replace(',', '.'))
326
- evaluation.ajouter_element(name.strip(), cat.strip(), pts_float)
327
- valid_elements_added += 1
328
- except ValueError:
329
- print(f"Skipping element due to invalid points format: Name='{name}', Pts='{pts}'")
330
  else:
331
- print(f"Skipping incomplete element: Name='{name}', Cat='{cat}', Pts='{pts}'")
332
-
333
- if valid_elements_added == 0:
334
- flash("Aucun élément technique valide n'a été ajouté. Veuillez vérifier les entrées.", "error")
335
- # Persist form data (more advanced, not implemented here simply)
336
- return redirect(url_for('index'))
337
 
 
 
 
 
338
 
339
  # --- Génération du document DOCX ---
340
- safe_candidat_name = "".join(c if c.isalnum() else "_" for c in nom_candidat)
341
- unique_id = uuid.uuid4().hex[:6] # Short unique ID
342
- base_filename = f"evaluation_{safe_candidat_name}_{session_value}_{unique_id}"
343
  docx_filename = f"{base_filename}.docx"
344
  docx_filepath = os.path.join(app.config['UPLOAD_FOLDER'], docx_filename)
345
 
346
- # This might raise an exception if saving fails
347
- evaluation.generer_document(docx_filepath)
 
 
 
348
 
349
  # --- Conversion et envoi ---
350
  if output_format == "pdf":
351
- if not convertapi.api_secret or convertapi.api_secret == 'YOUR_SECRET':
352
- flash("La clé API pour ConvertAPI n'est pas configurée. Impossible de générer le PDF.", "error")
353
- # Still have the DOCX, could offer that instead or redirect
354
- # Let's redirect back for now
355
- return redirect(url_for('index'))
356
-
357
  print(f"Attempting PDF conversion for {docx_filepath}...")
358
  try:
359
- # Explicitly set output path for saved PDF
360
- pdf_filename = f"{base_filename}.pdf"
361
- pdf_filepath = os.path.join(app.config['UPLOAD_FOLDER'], pdf_filename)
362
-
363
- result = convertapi.convert('pdf', {'File': docx_filepath}, from_format = 'docx')
364
-
365
- # Save the converted file(s)
366
- saved_files = result.save_files(app.config['UPLOAD_FOLDER']) # Save to temp folder
367
- print(f"ConvertAPI saved files: {saved_files}")
368
-
369
- # Find the expected PDF file path (ConvertAPI might rename slightly)
370
- # Let's assume the first saved file is the PDF we want if only one exists
371
- if saved_files and len(saved_files) == 1:
372
- actual_pdf_path = saved_files[0]
373
- # Optionally rename it back to our desired name if needed, but sending works
374
- print(f"PDF conversion successful: {actual_pdf_path}")
375
- return send_file(actual_pdf_path, as_attachment=True, download_name=pdf_filename)
376
- elif os.path.exists(pdf_filepath): # Check if it saved with exact name
377
- print(f"PDF conversion successful (exact path): {pdf_filepath}")
378
- return send_file(pdf_filepath, as_attachment=True, download_name=pdf_filename)
379
- else:
380
- flash(f"Erreur: Fichier PDF non trouvé après conversion. Fichiers sauvegardés: {saved_files}", "error")
381
- return redirect(url_for('index'))
382
-
383
  except Exception as e:
384
  print(f"Error during PDF conversion: {e}")
385
- flash(f"Erreur durant la conversion PDF: {e}. Vérifiez vos crédits ConvertAPI ou le fichier.", "error")
386
- # Fallback: Offer the DOCX instead? Or just redirect.
387
- return redirect(url_for('index'))
 
388
 
389
  else: # Send the generated DOCX
390
  return send_file(docx_filepath, as_attachment=True, download_name=docx_filename)
391
 
392
  except Exception as e:
393
- # Log the general error
394
  print(f"An error occurred during POST request processing: {e}")
 
395
  flash(f"Une erreur interne est survenue: {e}", "error")
396
- return redirect(url_for('index'))
397
 
398
  finally:
399
- # --- Cleanup ---
400
- # Clean up DOCX only if PDF was successfully sent or if DOCX was sent
401
- # If PDF conversion failed but DOCX exists, maybe keep it?
402
- # Let's clean up the DOCX if PDF was *attempted* regardless of success for simplicity
403
- if output_format == 'pdf' and docx_filepath and os.path.exists(docx_filepath):
404
- try:
405
- os.remove(docx_filepath)
406
- print(f"Cleaned up DOCX: {docx_filepath}")
407
- except OSError as e:
408
- print(f"Error cleaning up DOCX file {docx_filepath}: {e}")
409
- # We don't explicitly clean the PDF here as send_file handles it (or it might fail before sending)
 
 
410
 
411
 
412
  # --- Affichage du formulaire (GET request) ---
@@ -415,8 +518,11 @@ def index():
415
  if __name__ == "__main__":
416
  # Make sure the UPLOAD_FOLDER exists when running directly
417
  if not os.path.exists(UPLOAD_FOLDER):
418
- os.makedirs(UPLOAD_FOLDER)
419
- # Consider security implications of running debug=True in production
420
- app.run(debug=True, host='0.0.0.0', port=5001) # Run on port 5001 for example
 
 
421
 
422
- # --- END OF FLASK APP SCRIPT ---
 
 
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
 
9
  from docx.oxml.ns import nsdecls
10
  from docx.oxml import parse_xml
11
  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
 
18
  # Define a temporary directory for generated files
19
+ UPLOAD_FOLDER = 'temp_files'
 
20
  if not os.path.exists(UPLOAD_FOLDER):
21
+ try:
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"
 
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
 
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)
264
+ col_widths_recap = [];
265
+ for _ in range(5): col_widths_recap.extend([width_A_E_pair / 2, width_A_E_pair / 2])
266
+ col_widths_recap.extend([width_final_single, width_final_single, width_final_single])
267
+ current_total_width = sum(col_widths_recap); width_adjustment = (available_recap_width - current_total_width) / len(col_widths_recap)
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:
355
+ 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)
356
+ 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)
357
+ else:
358
+ for role in ["Projet", "Principal", "ROV"]:
359
+ 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)
360
+ run = para.add_run(f"Correcteur {role} : "); run.bold = True; run.font.size = Pt(self.dynamic_font_size)
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
370
  def modifier_serie(self, serie): self.serie = serie
371
  def modifier_etablissement(self, nom): self.etablissement = nom
372
  def modifier_session(self, annee): self.session = annee
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__)
 
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_secret or convertapi.api_secret == 'YOUR_SECRET':
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) ---
 
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