diff --git a/src/app/routes/settings.py b/src/app/routes/settings.py index 109a3a98..92c83d78 100644 --- a/src/app/routes/settings.py +++ b/src/app/routes/settings.py @@ -1,11 +1,10 @@ import json import os +import io from flask import Blueprint, request, render_template, flash, redirect, url_for, jsonify -from flask import send_from_directory, current_app +from flask import send_from_directory, current_app, send_file from flask_login import login_required, current_user -from flask_wtf.csrf import CSRFProtect - from ..forms import ( DeletePlanForm, UploadForm, @@ -22,8 +21,6 @@ from .evaluation import AISixLevelGridResponse from ...utils.decorator import role_required, roles_required, ensure_profile_completed -csrf = CSRFProtect() - # Importez bien sûr db et User depuis vos modèles from ..models import ( @@ -701,9 +698,63 @@ def download_canevas(filename): return send_from_directory(upload_folder, filename, as_attachment=True) +@settings_bp.route('/prompts/export', methods=['POST']) +@roles_required('admin') +@login_required +@ensure_profile_completed +def export_prompts(): + """Export all configured prompts into a single markdown file.""" + sections = SectionAISettings.query.all() + ocr = OcrPromptSettings.get_current() + plan_cadre_import = PlanCadreImportPromptSettings.get_current() + grille = GrillePromptSettings.get_current() + analyse = AnalysePlanCoursPrompt.query.first() + plan_de_cours_prompts = PlanDeCoursPromptSettings.query.all() + + lines = ["# Export des prompts\n"] + + if sections: + lines.append("## Sections IA\n") + for sa in sections: + lines.append(f"### {sa.section}\n\n```") + lines.append((sa.system_prompt or "") + "\n") + lines.append("```\n") + + if ocr and (ocr.extraction_prompt or '').strip(): + lines.append("## OCR Extraction\n\n```") + lines.append(ocr.extraction_prompt + "\n") + lines.append("```\n") + + if plan_cadre_import and (plan_cadre_import.prompt_template or '').strip(): + lines.append("## Import Plan-cadre\n\n```") + lines.append(plan_cadre_import.prompt_template + "\n") + lines.append("```\n") + + if grille and (grille.prompt_template or '').strip(): + lines.append("## Grille d'évaluation\n\n```") + lines.append(grille.prompt_template + "\n") + lines.append("```\n") + + if analyse and (analyse.prompt_template or '').strip(): + lines.append("## Analyse Plan de cours\n\n```") + lines.append(analyse.prompt_template + "\n") + lines.append("```\n") + + if plan_de_cours_prompts: + lines.append("## Plan de cours\n") + for p in plan_de_cours_prompts: + lines.append(f"### {p.field_name}\n\n```") + lines.append((p.prompt_template or "") + "\n") + lines.append("```\n") + + content = "".join(lines) + buffer = io.BytesIO(content.encode('utf-8')) + return send_file(buffer, as_attachment=True, download_name='prompts.md', mimetype='text/markdown') + + @settings_bp.route('/plan-de-cours/prompts', methods=['GET']) @roles_required('admin') -@login_required +@login_required @ensure_profile_completed def plan_de_cours_prompt_settings(): """Page de gestion des prompts Plan de cours. diff --git a/src/app/templates/parametres.html b/src/app/templates/parametres.html index d483879d..0b17bc50 100644 --- a/src/app/templates/parametres.html +++ b/src/app/templates/parametres.html @@ -83,6 +83,12 @@

Modèles OpenAI +
+ {{ csrf_token() }} + +
diff --git a/tests/test_prompts_export_route.py b/tests/test_prompts_export_route.py new file mode 100644 index 00000000..007b7bbd --- /dev/null +++ b/tests/test_prompts_export_route.py @@ -0,0 +1,62 @@ +from src.app.models import db, User, SectionAISettings, OcrPromptSettings +from werkzeug.security import generate_password_hash +from flask import url_for + + +def create_admin(app): + with app.app_context(): + admin = User( + username="admin", + password=generate_password_hash("pw"), + role="admin", + is_first_connexion=False, + ) + db.session.add(admin) + db.session.commit() + return admin.id + + +def login(client, user_id): + with client.session_transaction() as sess: + sess["_user_id"] = str(user_id) + sess["_fresh"] = True + + +def test_export_prompts_includes_section_and_ocr(app, client): + admin_id = create_admin(app) + login(client, admin_id) + + with app.app_context(): + sa = SectionAISettings.get_for("evaluation") + sa.system_prompt = "Eval Prompt" + ocr = OcrPromptSettings.get_current() + ocr.extraction_prompt = "OCR Prompt" + db.session.commit() + + # GET should be disallowed + assert client.get("/settings/prompts/export").status_code == 405 + resp = client.post("/settings/prompts/export") + assert resp.status_code == 200 + assert "attachment; filename=prompts.md" in resp.headers.get("Content-Disposition", "") + content = resp.data.decode() + assert "Eval Prompt" in content + assert "OCR Prompt" in content + + +def test_settings_sidebar_has_export_link(app, client): + admin_id = create_admin(app) + login(client, admin_id) + with app.test_request_context(): + export_url = url_for("settings.export_prompts") + resp = client.get("/settings/parametres") + assert resp.status_code == 200 + assert export_url.encode() in resp.data + + +def test_export_prompts_requires_csrf(app, client): + app.config["WTF_CSRF_ENABLED"] = True + admin_id = create_admin(app) + login(client, admin_id) + # Missing token should trigger CSRF failure + resp = client.post("/settings/prompts/export") + assert resp.status_code == 400