Docfile commited on
Commit
e1b7241
·
verified ·
1 Parent(s): f1fd51e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +35 -245
app.py CHANGED
@@ -11,12 +11,8 @@ from psycopg2.extras import RealDictCursor
11
  from google import genai
12
  from google.genai import types
13
  from utils import load_prompt
14
-
15
- # Nouveaux imports pour la génération PDF côté serveur
16
- import pdfkit
17
- from jinja2 import Template
18
- import tempfile
19
- import io
20
 
21
  # --- Configuration de l'application ---
22
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
@@ -34,16 +30,6 @@ DISSERTATIONS_FILE = os.path.join(DATA_DIR, "dissertations_log.json")
34
  # Créer le dossier data s'il n'existe pas
35
  os.makedirs(DATA_DIR, exist_ok=True)
36
 
37
- # Configuration wkhtmltopdf (ajustez le chemin selon votre système)
38
- # Sur Linux/Mac: wkhtmltopdf est généralement dans le PATH
39
- # Sur Windows: spécifiez le chemin complet vers wkhtmltopdf.exe
40
- WKHTML_PATH = os.environ.get('WKHTML_PATH', None) # None = utilise le PATH
41
-
42
- if WKHTML_PATH:
43
- config = pdfkit.configuration(wkhtmltopdf=WKHTML_PATH)
44
- else:
45
- config = pdfkit.configuration()
46
-
47
  # --- Modèles de Données Pydantic (inchangés) ---
48
  class Argument(BaseModel):
49
  paragraphe_argumentatif: str = Field(description="Un unique paragraphe formant un argument complet. Il doit commencer par un connecteur logique (ex: 'Premièrement,'), suivi de son développement.")
@@ -60,7 +46,7 @@ class Dissertation(BaseModel):
60
  parties: List[Partie]
61
  conclusion: str = Field(description="La conclusion complète de la dissertation.")
62
 
63
- # --- Configuration Gemini ---
64
  try:
65
  if not GOOGLE_API_KEY:
66
  logging.warning("La variable d'environnement TOKEN (GOOGLE_API_KEY) n'est pas définie.")
@@ -78,124 +64,6 @@ SAFETY_SETTINGS = [
78
  {"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "BLOCK_NONE"},
79
  ]
80
 
81
- # --- Template HTML pour le PDF ---
82
- PDF_TEMPLATE = """
83
- <!DOCTYPE html>
84
- <html lang="fr">
85
- <head>
86
- <meta charset="UTF-8">
87
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
88
- <title>Dissertation Philosophique</title>
89
- <link rel="preconnect" href="https://fonts.googleapis.com">
90
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
91
- <link href="https://fonts.googleapis.com/css2?family=Kalam&display=swap" rel="stylesheet">
92
- <style>
93
- body {
94
- margin: 0;
95
- padding: 0;
96
- font-family: 'Kalam', cursive;
97
- font-size: 20px;
98
- color: #1a2a4c;
99
- background-color: #fdfaf4;
100
- line-height: 2;
101
- }
102
-
103
- .dissertation-paper {
104
- background-image: linear-gradient(transparent 97%, #d8e2ee 98%);
105
- background-size: 100% 40px;
106
- border-left: 3px solid #ffaaab;
107
- padding-left: 4em;
108
- padding-top: 30px;
109
- padding-bottom: 40px;
110
- padding-right: 30px;
111
- min-height: 100vh;
112
- -webkit-print-color-adjust: exact;
113
- print-color-adjust: exact;
114
- }
115
-
116
- .dissertation-paper h2 {
117
- font-size: 1.5em;
118
- text-align: center;
119
- margin-bottom: 1.5em;
120
- color: #1a2a4c;
121
- }
122
-
123
- .dissertation-paper h3 {
124
- font-size: 1.2em;
125
- margin-top: 3em;
126
- margin-bottom: 1.5em;
127
- text-transform: uppercase;
128
- text-decoration: underline;
129
- color: #1a2a4c;
130
- }
131
-
132
- .dissertation-paper .development-block {
133
- margin-top: 3em;
134
- }
135
-
136
- .dissertation-paper p {
137
- text-align: justify;
138
- margin: 0;
139
- padding: 0;
140
- }
141
-
142
- .dissertation-paper .prof {
143
- text-align: center;
144
- font-style: italic;
145
- margin-bottom: 2em;
146
- }
147
-
148
- .dissertation-paper .indented {
149
- text-indent: 3em;
150
- }
151
-
152
- .dissertation-paper .transition {
153
- margin-top: 2em;
154
- margin-bottom: 2em;
155
- font-style: italic;
156
- color: #4a6a9c;
157
- }
158
-
159
- .avoid-page-break {
160
- page-break-inside: avoid;
161
- break-inside: avoid;
162
- }
163
-
164
- @page {
165
- margin: 15mm;
166
- size: A4;
167
- }
168
- </style>
169
- </head>
170
- <body>
171
- <div class="dissertation-paper">
172
- <h2>Sujet : {{ dissertation.sujet }}</h2>
173
- <p class="prof">Prof : {{ dissertation.prof }}</p>
174
-
175
- <h3>Introduction</h3>
176
- <p class="indented">{{ dissertation.introduction }}</p>
177
-
178
- {% for partie in dissertation.parties %}
179
- <div class="avoid-page-break">
180
- <div class="development-block">
181
- <p class="indented">{{ partie.chapeau }}</p>
182
- {% for arg in partie.arguments %}
183
- <p class="indented">{{ arg.paragraphe_argumentatif }}</p>
184
- {% endfor %}
185
- </div>
186
- {% if partie.transition %}
187
- <p class="indented transition">{{ partie.transition }}</p>
188
- {% endif %}
189
- </div>
190
- {% endfor %}
191
-
192
- <h3>Conclusion</h3>
193
- <p class="indented">{{ dissertation.conclusion }}</p>
194
- </div>
195
- </body>
196
- </html>
197
- """
198
-
199
  # --- Helpers de base de données (inchangés) ---
200
  def create_connection():
201
  """Crée et retourne une connexion à la base de données PostgreSQL."""
@@ -250,39 +118,7 @@ def load_dissertations_data():
250
  logging.error(f"Erreur lors du chargement des données: {e}")
251
  return []
252
 
253
- # --- NOUVELLE fonction pour générer le PDF côté serveur ---
254
- def generate_pdf_from_dissertation(dissertation_data):
255
- """Génère un PDF à partir des données de dissertation."""
256
- try:
257
- # Créer le template Jinja2
258
- template = Template(PDF_TEMPLATE)
259
-
260
- # Rendre le HTML avec les données
261
- html_content = template.render(dissertation=dissertation_data)
262
-
263
- # Options pour wkhtmltopdf
264
- options = {
265
- 'page-size': 'A4',
266
- 'margin-top': '15mm',
267
- 'margin-right': '15mm',
268
- 'margin-bottom': '15mm',
269
- 'margin-left': '15mm',
270
- 'encoding': "UTF-8",
271
- 'no-outline': None,
272
- 'enable-local-file-access': None,
273
- 'print-media-type': None
274
- }
275
-
276
- # Générer le PDF en mémoire
277
- pdf_bytes = pdfkit.from_string(html_content, False, options=options, configuration=config)
278
-
279
- return pdf_bytes
280
-
281
- except Exception as e:
282
- logging.error(f"Erreur lors de la génération PDF: {e}")
283
- raise
284
-
285
- # --- Routes (inchangées sauf nouvelle route PDF) ---
286
  @app.route('/')
287
  def philosophie():
288
  return render_template("philosophie.html")
@@ -291,80 +127,44 @@ def philosophie():
291
  def gestion():
292
  return render_template("gestion.html")
293
 
 
294
  @app.route('/api/gestion/dissertations', methods=['GET'])
295
  def get_dissertations_data():
296
- """Récupère toutes les données des dissertations générées."""
297
  try:
298
  data = load_dissertations_data()
299
- return jsonify({
300
- "success": True,
301
- "data": data,
302
- "total": len(data)
303
- })
304
  except Exception as e:
305
  logging.error(f"Erreur lors de la récupération des données de gestion: {e}")
306
- return jsonify({
307
- "success": False,
308
- "error": "Erreur lors de la récupération des données"
309
- }), 500
310
 
311
  @app.route('/api/gestion/dissertations/<int:record_id>', methods=['DELETE'])
312
  def delete_dissertation_record(record_id):
313
- """Supprime un enregistrement spécifique."""
314
  try:
315
  data = load_dissertations_data()
316
-
317
- record_index = None
318
- for i, record in enumerate(data):
319
- if record.get('id') == record_id:
320
- record_index = i
321
- break
322
-
323
  if record_index is None:
324
- return jsonify({
325
- "success": False,
326
- "error": "Enregistrement non trouvé"
327
- }), 404
328
-
329
- deleted_record = data.pop(record_index)
330
-
331
  with open(DISSERTATIONS_FILE, 'w', encoding='utf-8') as f:
332
  json.dump(data, f, ensure_ascii=False, indent=2)
333
-
334
- return jsonify({
335
- "success": True,
336
- "message": "Enregistrement supprimé avec succès"
337
- })
338
-
339
  except Exception as e:
340
  logging.error(f"Erreur lors de la suppression: {e}")
341
- return jsonify({
342
- "success": False,
343
- "error": "Erreur lors de la suppression"
344
- }), 500
345
 
346
  @app.route('/api/gestion/dissertations/clear', methods=['DELETE'])
347
  def clear_all_dissertations():
348
- """Vide toutes les données des dissertations."""
349
  try:
350
  with open(DISSERTATIONS_FILE, 'w', encoding='utf-8') as f:
351
  json.dump([], f)
352
-
353
- return jsonify({
354
- "success": True,
355
- "message": "Toutes les données ont été supprimées"
356
- })
357
-
358
  except Exception as e:
359
  logging.error(f"Erreur lors de la suppression générale: {e}")
360
- return jsonify({
361
- "success": False,
362
- "error": "Erreur lors de la suppression"
363
- }), 500
364
 
 
365
  @app.route('/api/philosophy/courses', methods=['GET'])
366
  def get_philosophy_courses():
367
- """Récupère la liste de tous les cours de philosophie pour le menu déroulant."""
368
  conn = create_connection()
369
  if not conn:
370
  return jsonify({"error": "Connexion à la base de données échouée."}), 503
@@ -380,6 +180,7 @@ def get_philosophy_courses():
380
  if conn:
381
  conn.close()
382
 
 
383
  @app.route('/api/generate_dissertation', methods=['POST'])
384
  def generate_dissertation_api():
385
  if not client:
@@ -402,7 +203,6 @@ def generate_dissertation_api():
402
  save_dissertation_data(data, None, False, error_msg)
403
  return jsonify({"error": error_msg}), 400
404
 
405
- # Récupérer le contenu du cours si un ID est fourni
406
  context_str = ""
407
  if course_id:
408
  conn = create_connection()
@@ -433,13 +233,11 @@ def generate_dissertation_api():
433
  return jsonify({"error": error_msg}), 500
434
 
435
  final_prompt = prompt_template.format(phi_prompt=sujet, context=context_str)
436
-
437
  config = types.GenerateContentConfig(
438
  safety_settings=SAFETY_SETTINGS,
439
  response_mime_type="application/json",
440
  response_schema=Dissertation,
441
  )
442
-
443
  response = client.models.generate_content(
444
  model="gemini-2.5-flash",
445
  contents=final_prompt,
@@ -462,42 +260,34 @@ def generate_dissertation_api():
462
  save_dissertation_data(data, None, False, error_msg)
463
  return jsonify({"error": error_msg}), 500
464
 
465
- # --- NOUVELLE Route pour générer et télécharger le PDF ---
466
  @app.route('/api/generate_pdf', methods=['POST'])
467
  def generate_pdf_api():
468
- """Génère et retourne un PDF de dissertation côté serveur."""
 
 
 
 
469
  try:
470
- data = request.json
471
-
472
- if not data:
473
- return jsonify({"error": "Aucune donnée de dissertation fournie."}), 400
474
-
475
- # Vérifier que les données contiennent les champs nécessaires
476
- required_fields = ['sujet', 'prof', 'introduction', 'parties', 'conclusion']
477
- for field in required_fields:
478
- if field not in data:
479
- return jsonify({"error": f"Champ manquant: {field}"}), 400
480
-
481
- # Générer le PDF
482
- pdf_bytes = generate_pdf_from_dissertation(data)
483
-
484
- # Créer un nom de fichier basé sur le sujet (limité et sécurisé)
485
- safe_filename = "".join(c for c in data['sujet'][:50] if c.isalnum() or c in (' ', '-', '_')).strip()
486
- if not safe_filename:
487
- safe_filename = "dissertation"
488
- filename = f"{safe_filename}.pdf"
489
-
490
- # Retourner le PDF
491
  return send_file(
492
  io.BytesIO(pdf_bytes),
493
  mimetype='application/pdf',
494
  as_attachment=True,
495
- download_name=filename
496
  )
497
-
498
  except Exception as e:
499
- logging.error(f"Erreur lors de la génération du PDF: {e}")
500
- return jsonify({"error": "Erreur lors de la génération du PDF."}), 500
 
501
 
502
  if __name__ == '__main__':
503
  app.run(debug=True, port=5001)
 
11
  from google import genai
12
  from google.genai import types
13
  from utils import load_prompt
14
+ from weasyprint import HTML # <-- NOUVEL IMPORT
15
+ import io # <-- NOUVEL IMPORT
 
 
 
 
16
 
17
  # --- Configuration de l'application ---
18
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
 
30
  # Créer le dossier data s'il n'existe pas
31
  os.makedirs(DATA_DIR, exist_ok=True)
32
 
 
 
 
 
 
 
 
 
 
 
33
  # --- Modèles de Données Pydantic (inchangés) ---
34
  class Argument(BaseModel):
35
  paragraphe_argumentatif: str = Field(description="Un unique paragraphe formant un argument complet. Il doit commencer par un connecteur logique (ex: 'Premièrement,'), suivi de son développement.")
 
46
  parties: List[Partie]
47
  conclusion: str = Field(description="La conclusion complète de la dissertation.")
48
 
49
+ # --- Configuration Gemini (inchangée) ---
50
  try:
51
  if not GOOGLE_API_KEY:
52
  logging.warning("La variable d'environnement TOKEN (GOOGLE_API_KEY) n'est pas définie.")
 
64
  {"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "BLOCK_NONE"},
65
  ]
66
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
  # --- Helpers de base de données (inchangés) ---
68
  def create_connection():
69
  """Crée et retourne une connexion à la base de données PostgreSQL."""
 
118
  logging.error(f"Erreur lors du chargement des données: {e}")
119
  return []
120
 
121
+ # --- Routes (inchangées) ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
  @app.route('/')
123
  def philosophie():
124
  return render_template("philosophie.html")
 
127
  def gestion():
128
  return render_template("gestion.html")
129
 
130
+ # --- API de gestion (inchangées) ---
131
  @app.route('/api/gestion/dissertations', methods=['GET'])
132
  def get_dissertations_data():
 
133
  try:
134
  data = load_dissertations_data()
135
+ return jsonify({"success": True, "data": data, "total": len(data)})
 
 
 
 
136
  except Exception as e:
137
  logging.error(f"Erreur lors de la récupération des données de gestion: {e}")
138
+ return jsonify({"success": False, "error": "Erreur lors de la récupération des données"}), 500
 
 
 
139
 
140
  @app.route('/api/gestion/dissertations/<int:record_id>', methods=['DELETE'])
141
  def delete_dissertation_record(record_id):
 
142
  try:
143
  data = load_dissertations_data()
144
+ record_index = next((i for i, record in enumerate(data) if record.get('id') == record_id), None)
 
 
 
 
 
 
145
  if record_index is None:
146
+ return jsonify({"success": False, "error": "Enregistrement non trouvé"}), 404
147
+ data.pop(record_index)
 
 
 
 
 
148
  with open(DISSERTATIONS_FILE, 'w', encoding='utf-8') as f:
149
  json.dump(data, f, ensure_ascii=False, indent=2)
150
+ return jsonify({"success": True, "message": "Enregistrement supprimé avec succès"})
 
 
 
 
 
151
  except Exception as e:
152
  logging.error(f"Erreur lors de la suppression: {e}")
153
+ return jsonify({"success": False, "error": "Erreur lors de la suppression"}), 500
 
 
 
154
 
155
  @app.route('/api/gestion/dissertations/clear', methods=['DELETE'])
156
  def clear_all_dissertations():
 
157
  try:
158
  with open(DISSERTATIONS_FILE, 'w', encoding='utf-8') as f:
159
  json.dump([], f)
160
+ return jsonify({"success": True, "message": "Toutes les données ont été supprimées"})
 
 
 
 
 
161
  except Exception as e:
162
  logging.error(f"Erreur lors de la suppression générale: {e}")
163
+ return jsonify({"success": False, "error": "Erreur lors de la suppression"}), 500
 
 
 
164
 
165
+ # --- API pour lister les cours (inchangée) ---
166
  @app.route('/api/philosophy/courses', methods=['GET'])
167
  def get_philosophy_courses():
 
168
  conn = create_connection()
169
  if not conn:
170
  return jsonify({"error": "Connexion à la base de données échouée."}), 503
 
180
  if conn:
181
  conn.close()
182
 
183
+ # --- API pour la génération de dissertation (inchangée) ---
184
  @app.route('/api/generate_dissertation', methods=['POST'])
185
  def generate_dissertation_api():
186
  if not client:
 
203
  save_dissertation_data(data, None, False, error_msg)
204
  return jsonify({"error": error_msg}), 400
205
 
 
206
  context_str = ""
207
  if course_id:
208
  conn = create_connection()
 
233
  return jsonify({"error": error_msg}), 500
234
 
235
  final_prompt = prompt_template.format(phi_prompt=sujet, context=context_str)
 
236
  config = types.GenerateContentConfig(
237
  safety_settings=SAFETY_SETTINGS,
238
  response_mime_type="application/json",
239
  response_schema=Dissertation,
240
  )
 
241
  response = client.models.generate_content(
242
  model="gemini-2.5-flash",
243
  contents=final_prompt,
 
260
  save_dissertation_data(data, None, False, error_msg)
261
  return jsonify({"error": error_msg}), 500
262
 
263
+ # --- NOUVELLE ROUTE API POUR LA GÉNÉRATION DE PDF ---
264
  @app.route('/api/generate_pdf', methods=['POST'])
265
  def generate_pdf_api():
266
+ """Génère un PDF à partir des données JSON de la dissertation."""
267
+ dissertation_data = request.json
268
+ if not dissertation_data:
269
+ return jsonify({"error": "Aucune donnée de dissertation fournie."}), 400
270
+
271
  try:
272
+ # 1. Rendre le template HTML avec les données de la dissertation
273
+ html_string = render_template('dissertation_pdf.html', dissertation=dissertation_data)
274
+
275
+ # 2. Utiliser WeasyPrint pour convertir le HTML en PDF
276
+ html = HTML(string=html_string)
277
+ pdf_bytes = html.write_pdf()
278
+
279
+ # 3. Envoyer le PDF en tant que fichier à télécharger
 
 
 
 
 
 
 
 
 
 
 
 
 
280
  return send_file(
281
  io.BytesIO(pdf_bytes),
282
  mimetype='application/pdf',
283
  as_attachment=True,
284
+ download_name='dissertation-philosophie.pdf'
285
  )
286
+
287
  except Exception as e:
288
+ logging.error(f"Erreur lors de la génération du PDF : {e}")
289
+ return jsonify({"error": "Une erreur est survenue lors de la création du PDF."}), 500
290
+
291
 
292
  if __name__ == '__main__':
293
  app.run(debug=True, port=5001)