Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
fa556d6
Improve DOCX schema streaming
fpoisson2 Aug 31, 2025
9039750
Render JSON schema with nested accordions
fpoisson2 Aug 31, 2025
3e7ccdd
refine docx schema prompt and add graph
fpoisson2 Aug 31, 2025
bd95b8a
Improve DOCX schema streaming and mobile graph
fpoisson2 Aug 31, 2025
461ce66
Visualize custom schema tree
fpoisson2 Aug 31, 2025
bd8be3a
test: cover parts/fields normalization
fpoisson2 Aug 31, 2025
e925c44
Add validation workflow for DOCX schema
fpoisson2 Aug 31, 2025
a350d79
Persist DOCX schema pages with admin management
fpoisson2 Aug 31, 2025
0dc5d4d
feat: show persisted schema with accordion preview
fpoisson2 Aug 31, 2025
7190a9b
Use hierarchical tree for schema graph
fpoisson2 Aug 31, 2025
d01c993
Handle JSON schema properties in preview graphs
fpoisson2 Aug 31, 2025
209fa47
Color and draggable schema graph
fpoisson2 Aug 31, 2025
3017e87
Move DOCX schema links to new settings section
fpoisson2 Sep 1, 2025
c8443ee
Refactor DOCX schema workflow and settings
fpoisson2 Sep 1, 2025
4da27a8
Prefill default DOCX schema prompt and tidy settings links
fpoisson2 Sep 1, 2025
91c974c
Move DOCX schema links to settings menu and fix mobile graph
fpoisson2 Sep 1, 2025
a588561
Restore schema links in navbar
fpoisson2 Sep 1, 2025
89e08ff
Expand schema graph and add editing
fpoisson2 Sep 1, 2025
1e6e257
Center schema graph with initial D3 zoom
fpoisson2 Sep 1, 2025
a17bc61
Capture Markdown alongside JSON schemas
fpoisson2 Sep 1, 2025
233ad78
feat: request structured schema output
fpoisson2 Sep 1, 2025
2e6d5ee
fix: use text_format for docx schema task
fpoisson2 Sep 1, 2025
8541dea
fix: use response_format for DOCX schema tasks
fpoisson2 Sep 1, 2025
a3d21fc
fix: use text_format for DOCX schema
fpoisson2 Sep 1, 2025
6720eb8
docs: speed up tests in AGENTS
fpoisson2 Sep 1, 2025
8457144
Merge pull request #196 from fpoisson2/codex/add-docx-to-json-convers…
fpoisson2 Sep 1, 2025
707b056
Allow renaming imported docx schema
fpoisson2 Sep 1, 2025
855cbc4
Add schema prompt settings page and tests
fpoisson2 Sep 1, 2025
7652db7
feat: add plan cadre form
fpoisson2 Sep 1, 2025
886df27
Test CSRF token on rename endpoint
fpoisson2 Sep 1, 2025
64424b3
Merge pull request #197 from fpoisson2/codex/allow-changing-title-of-…
fpoisson2 Sep 1, 2025
59db77a
feat: order plan form and handle nested arrays
fpoisson2 Sep 1, 2025
3dd419e
fix: honor markdown order in schema preview
fpoisson2 Sep 1, 2025
2f4a558
feat: respect markdown order in plan form
fpoisson2 Sep 1, 2025
c5c7ab5
fix: compute markdown order on load
fpoisson2 Sep 1, 2025
88117cc
fix: normalize markdown ordering
fpoisson2 Sep 1, 2025
13dc733
fix: align form fields with markdown order
fpoisson2 Sep 1, 2025
db76f43
fix: honor markdown order for nested fields
fpoisson2 Sep 1, 2025
8d152b5
Merge pull request #200 from fpoisson2/codex/mise-en-page-des-informa…
fpoisson2 Sep 1, 2025
6eda5d5
Merge pull request #198 from fpoisson2/codex/add-ai-settings-page-for…
fpoisson2 Sep 1, 2025
88ea907
Move docx schema details to JSON page
fpoisson2 Sep 2, 2025
00964ea
Allow PDF import for schema conversion
fpoisson2 Sep 2, 2025
3fe6672
Merge pull request #202 from fpoisson2/codex/move-elements-to-/json-p…
fpoisson2 Sep 2, 2025
0bf3f43
Merge pull request #203 from fpoisson2/codex/add-pdf-import-for-conve…
fpoisson2 Sep 2, 2025
d1af38e
docs: clarify how to get pytest summary
fpoisson2 Sep 2, 2025
e867685
Merge pull request #205 from fpoisson2/codex/add-instructions-for-tes…
fpoisson2 Sep 2, 2025
c67b5d3
Add tabs for docx schema prompts
fpoisson2 Sep 2, 2025
51f9b16
Merge pull request #206 from fpoisson2/codex/add-import/generate/impr…
fpoisson2 Sep 2, 2025
90f3841
feat: enhance docx schema preview styling
fpoisson2 Sep 2, 2025
44611c7
Merge pull request #207 from fpoisson2/codex/update-docx-schema-page-…
fpoisson2 Sep 2, 2025
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
7 changes: 5 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ Ce référentiel utilise des agents/outils pour automatiser des modifications de

## Règle essentielle
- Toujours écrire des tests pertinents (pytest) pour couvrir le correctif ou la fonctionnalité ajoutée.
- Toujours exécuter la suite de tests localement avec `pytest -q` et s'assurer qu'elle passe avant de conclure la tâche.
- Toujours exécuter la suite de tests localement avec `PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 pytest -q` pour accélérer l'exécution et s'assurer qu'elle passe avant de conclure la tâche.
- Inclure et valider un jeton CSRF pour toute requête POST/PUT/DELETE modifiant l'état (champ `csrf_token` ou en-tête `X-CSRFToken`).

## Détails pratiques
- Emplacement des tests: placez-les sous `tests/` avec le préfixe `test_*.py`.
Expand All @@ -15,7 +16,9 @@ Ce référentiel utilise des agents/outils pour automatiser des modifications de
- Si des avertissements perturbent la lisibilité, nettoyez-les ou filtrez-les de manière ciblée, sans masquer des problèmes réels.

## Commandes utiles
- Lancer toute la suite: `pytest -q`
- Lancer toute la suite (résumé concis): `PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 pytest -q`
- Pour obtenir explicitement le résumé final (nombre de tests et durée), exécutez `PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 pytest` sans `-q` supplémentaire (le fichier `pytest.ini` l'inclut déjà) et relevez la dernière ligne de sortie.
- Interrompre au premier échec pour un diagnostic rapide: `PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 pytest -q --maxfail=1`
- Exécuter un seul fichier: `pytest -q tests/test_mon_module.py`
- Exécuter un seul test: `pytest -q tests/test_mon_module.py::test_cas_specifique`
- Compter les tests rapidement (collect only): `pytest -q --collect-only | awk -F': ' '{s+=$2} END{print s}'`
Expand Down
1 change: 1 addition & 0 deletions OPENAI_USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Note: L’ancien endpoint synchrone `POST /gestion_programme/update_verifier_pla
- Grille d’évaluation: `src/app/tasks/generation_grille.py`
- Logigramme de compétences: `src/app/tasks/generation_logigramme.py`
- OCR/Imports: `src/app/tasks/ocr.py`, `src/app/tasks/import_plan_de_cours.py`, `src/app/tasks/import_grille.py`, `src/app/tasks/import_plan_cadre.py`
- Conversion DOCX→Schéma JSON: `src/app/tasks/docx_to_schema.py` (start `POST /docx_to_schema/start`)

Tous suivent le pattern:

Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ python-dotenv
pytz
redis
reportlab
pytest-asyncio
requests
starlette
tiktoken
Expand Down
12 changes: 11 additions & 1 deletion src/app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
from .routes import competences_management # noqa: F401
from .routes import fil_conducteur_routes # noqa: F401
from .routes import settings_departements # noqa: F401
from .routes import admin_docx_schema # noqa: F401
from .routes.chat import chat
# Import blueprints
from .routes.cours import cours_bp
Expand All @@ -58,7 +59,6 @@
from .routes.api import api_bp
from .routes.oauth import oauth_bp
from .routes.tasks import tasks_bp
from ..mcp_server.server import init_app as init_mcp_server

# Import version
from ..config.version import __version__
Expand Down Expand Up @@ -246,6 +246,7 @@ def load_user(user_id):
init_change_tracking(db)

# Bind Flask app to MCP server for OAuth verification
from ..mcp_server.server import init_app as init_mcp_server
init_mcp_server(app)

if not testing:
Expand Down Expand Up @@ -297,6 +298,15 @@ def asset_url(path: str) -> str:
# Expose csrf_token() helper globally for templates
return dict(has_endpoint=has_endpoint, asset_url=asset_url, csrf_token=generate_csrf)

@app.context_processor
def inject_docx_schema_pages():
try:
from .models import DocxSchemaPage
pages = DocxSchemaPage.query.order_by(DocxSchemaPage.created_at.asc()).all()
except Exception:
pages = []
return dict(docx_schema_pages=pages)

@app.before_request
def before_request():
# Allow static files and explicitly public routes to bypass auth redirect
Expand Down
5 changes: 5 additions & 0 deletions src/app/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ class FileUploadForm(FlaskForm):
file = FileField("Importez un fichier PDF", validators=[DataRequired()])
submit = SubmitField("Envoyer")


class DocxToSchemaForm(FlaskForm):
file = FileField("Fichier DOCX ou PDF", validators=[DataRequired()])
submit = SubmitField("Convertir")

class AssociateDevisForm(FlaskForm):
base_filename = HiddenField(validators=[DataRequired()])
programme_id = SelectField("Choisir le Programme Cible :", coerce=int, validators=[DataRequired()])
Expand Down
11 changes: 11 additions & 0 deletions src/app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,17 @@ def get_current(cls):
db.session.rollback()
return obj

class DocxSchemaPage(db.Model):
"""Page générée à partir d'une validation de schéma DOCX."""
__tablename__ = 'docx_schema_pages'

id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(255), nullable=True)
json_schema = db.Column(db.JSON, nullable=False)
markdown_content = db.Column(db.Text, nullable=True)
created_at = db.Column(db.DateTime, default=now_utc)


class EvaluationSavoirFaire(db.Model):
__tablename__ = 'evaluation_savoirfaire'

Expand Down
178 changes: 178 additions & 0 deletions src/app/routes/admin_docx_schema.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import os
import re
import time
from flask import render_template, request, jsonify, current_app, redirect, url_for, session
from flask_login import login_required, current_user

from ..forms import DocxToSchemaForm
from ..tasks.docx_to_schema import docx_to_json_schema_task
from ..models import db, DocxSchemaPage, SectionAISettings
from .routes import main
from ...utils.decorator import role_required, ensure_profile_completed

DEFAULT_DOCX_TO_SCHEMA_PROMPT = (
"Propose un schéma JSON simple, cohérent et normalisé pour représenter parfaitement ce document. "
"Retourne un objet structuré avec quatre clés : `title`, `description`, `schema` et `markdown`. "
"`schema` contient le schéma JSON complet, `markdown` une version Markdown fidèle au document. "
"Chaque champ du schéma doit inclure un titre et une description et la hiérarchie doit être respectée. "
"Ne retourne que cet objet JSON."
)


@main.route('/docx_to_schema', methods=['GET'])
@role_required('admin')
@ensure_profile_completed
def docx_to_schema_page():
form = DocxToSchemaForm()
return render_template('settings/docx_to_schema.html', form=form)


@main.route('/docx_to_schema/start', methods=['POST'])
@role_required('admin')
@ensure_profile_completed
def docx_to_schema_start():
form = DocxToSchemaForm()
if not form.validate_on_submit():
return jsonify({'error': 'Invalid submission.', 'details': form.errors}), 400

file = form.file.data
if not file or not file.filename.lower().endswith(('.docx', '.pdf')):
return jsonify({'error': 'Veuillez fournir un fichier .docx ou .pdf.'}), 400

upload_dir = os.path.join(current_app.config.get('UPLOAD_FOLDER', 'uploads'))
os.makedirs(upload_dir, exist_ok=True)
safe_name = re.sub(r'[^A-Za-z0-9_.-]+', '_', file.filename)
stored_name = f"docx_schema_{int(time.time())}_{safe_name}"
stored_path = os.path.join(upload_dir, stored_name)
file.save(stored_path)

sa = SectionAISettings.get_for('docx_to_schema')
model = sa.ai_model or 'gpt-4o-mini'
reasoning = sa.reasoning_effort or 'medium'
verbosity = sa.verbosity or 'medium'
system_prompt = sa.system_prompt or DEFAULT_DOCX_TO_SCHEMA_PROMPT

task = docx_to_json_schema_task.delay(stored_path, model, reasoning, verbosity, system_prompt, current_user.id)
return jsonify({'task_id': task.id}), 202


@main.route('/docx_to_schema/preview', methods=['GET', 'POST'])
@role_required('admin')
@ensure_profile_completed
def docx_to_schema_preview_temp():
if request.method == 'POST':
data = request.get_json() or {}
schema = data.get('schema')
title = data.get('title')
description = data.get('description')
if isinstance(schema, dict):
if title and 'title' not in schema:
schema['title'] = title
if description and 'description' not in schema:
schema['description'] = description
session['pending_docx_schema'] = schema
session['pending_docx_markdown'] = data.get('markdown')
session['pending_docx_title'] = title
session['pending_docx_description'] = description
return jsonify({'ok': True})
schema = session.get('pending_docx_schema')
markdown = session.get('pending_docx_markdown')
title = session.get('pending_docx_title')
description = session.get('pending_docx_description')
if not schema:
return redirect(url_for('main.docx_to_schema_page'))
return render_template('docx_schema_validate.html', schema=schema, markdown=markdown, title=title, description=description)


@main.route('/docx_to_schema/validate', methods=['POST'])
@role_required('admin')
@ensure_profile_completed
def docx_to_schema_validate():
"""Persiste le schéma validé et retourne l'identifiant de la nouvelle page."""
data = request.get_json() or {}
schema = data.get('schema')
markdown = data.get('markdown')
if not schema:
return jsonify({'error': 'Schéma manquant.'}), 400

title = data.get('title') or schema.get('title') or schema.get('titre') or f"Schéma {int(time.time())}"
description = data.get('description') or schema.get('description')
if isinstance(schema, dict):
if title and 'title' not in schema:
schema['title'] = title
if description and 'description' not in schema:
schema['description'] = description
page = DocxSchemaPage(title=title, json_schema=schema, markdown_content=markdown)
db.session.add(page)
db.session.commit()
session.pop('pending_docx_schema', None)
session.pop('pending_docx_markdown', None)
session.pop('pending_docx_title', None)
session.pop('pending_docx_description', None)
return jsonify({'success': True, 'page_id': page.id}), 201

@main.route('/docx_schema', methods=['GET'])
@role_required('admin')
@ensure_profile_completed
def docx_schema_pages():
pages = DocxSchemaPage.query.order_by(DocxSchemaPage.created_at.desc()).all()
return render_template('docx_schema_list.html', pages=pages)


@main.route('/docx_schema/<int:page_id>', methods=['GET'])
@role_required('admin')
@ensure_profile_completed
def docx_schema_page_view(page_id):
page = DocxSchemaPage.query.get_or_404(page_id)
return render_template('docx_schema_preview.html', page=page)


@main.route('/docx_schema/<int:page_id>/json', methods=['GET'])
@role_required('admin')
@ensure_profile_completed
def docx_schema_page_json(page_id):
page = DocxSchemaPage.query.get_or_404(page_id)
return render_template('docx_schema_json.html', page=page)


@main.route('/docx_schema/<int:page_id>/edit', methods=['POST'])
@role_required('admin')
@ensure_profile_completed
def docx_schema_page_edit(page_id):
"""Met à jour le schéma JSON d'une page existante."""
page = DocxSchemaPage.query.get_or_404(page_id)
data = request.get_json() or {}
schema = data.get('schema')
if not schema:
return jsonify({'error': 'Schéma manquant.'}), 400
page.json_schema = schema
page.title = schema.get('title') or schema.get('titre') or page.title
db.session.commit()
return jsonify({'success': True})


@main.route('/docx_schema/<int:page_id>/rename', methods=['POST'])
@role_required('admin')
@ensure_profile_completed
def docx_schema_page_rename(page_id):
"""Met à jour uniquement le titre d'un schéma existant."""
page = DocxSchemaPage.query.get_or_404(page_id)
data = request.get_json() or {}
title = data.get('title')
if not title:
return jsonify({'error': 'Titre manquant.'}), 400
page.title = title
if isinstance(page.json_schema, dict):
page.json_schema['title'] = title
db.session.commit()
return jsonify({'success': True})


@main.route('/docx_schema/<int:page_id>/delete', methods=['POST'])
@role_required('admin')
@ensure_profile_completed
def docx_schema_page_delete(page_id):
page = DocxSchemaPage.query.get_or_404(page_id)
db.session.delete(page)
db.session.commit()
return redirect(url_for('main.docx_schema_pages'))
11 changes: 10 additions & 1 deletion src/app/routes/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@
ElementCompetenceParCours,
Cours,
CoursProgramme,
ListeCegep
ListeCegep,
DocxSchemaPage
)
from ...extensions import limiter
from ...utils.decorator import role_required, roles_required, ensure_profile_completed
Expand All @@ -80,6 +81,14 @@ def version():
from ...config.version import __version__
return jsonify({'version': __version__})


@main.route('/parametres')
@login_required
@ensure_profile_completed
def parametres_alias():
docx_schemas = DocxSchemaPage.query.order_by(DocxSchemaPage.created_at.desc()).all()
return render_template('parametres.html', docx_schemas=docx_schemas)

# Public: Health endpoint
@main.route('/health')
@public_route
Expand Down
Loading