Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 57 additions & 6 deletions src/app/routes/settings.py
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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 (
Expand Down Expand Up @@ -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.
Expand Down
6 changes: 6 additions & 0 deletions src/app/templates/parametres.html
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,12 @@ <h2 class="accordion-header">
<a href="{{ url_for('settings.manage_openai_models') }}" class="list-group-item list-group-item-action {% if request.endpoint == 'settings.manage_openai_models' %}active{% endif %}">
<i class="bi bi-cpu me-2"></i>Modèles OpenAI
</a>
<form action="{{ url_for('settings.export_prompts') }}" method="post">
{{ csrf_token() }}
<button type="submit" class="list-group-item list-group-item-action border-0 bg-transparent w-100 text-start">
<i class="bi bi-download me-2"></i>Exporter les prompts
</button>
</form>
</div>
</div>
</div>
Expand Down
62 changes: 62 additions & 0 deletions tests/test_prompts_export_route.py
Original file line number Diff line number Diff line change
@@ -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