gmao/app_new/pronote/routes.py

262 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Pronote Routes - GMAO Collège
Intégration Pronote
"""
import logging
from flask import Blueprint, render_template, redirect, url_for, request, flash, jsonify
from flask_login import login_required, current_user
from app_new.extensions import db, csrf
from ..pronote.models import PronoteSession
from ..core.models.college import Room, RoomSchedule
import os
import json
from datetime import datetime, date, timedelta
logger = logging.getLogger(__name__)
pronote_bp = Blueprint('pronote', __name__, template_folder='templates')
@pronote_bp.route('/')
@login_required
def index():
"""Page principale Pronote."""
session = PronoteSession.get()
return render_template('pronote/index.html', session=session)
@pronote_bp.route('/connect', methods=['GET', 'POST'])
@login_required
def connect():
"""Connexion à Pronote via QR Code."""
if request.method == 'POST':
qr_code = request.form.get('qr_code', '').strip()
pin = request.form.get('pin', '').strip()
account_pin = request.form.get('account_pin', '').strip() or None
if not qr_code or not pin:
flash('QR Code et PIN sont requis.', 'danger')
return redirect(url_for('pronote.connect'))
try:
from app_new.lib_ext.pronote_client import connect_with_qr, save_session_to_db
client = connect_with_qr(qr_code, pin, account_pin)
if client:
try:
creds = client.export_credentials()
session_data = {
'token': creds.get('password') or creds.get('token'),
'url': creds.get('pronote_url') or creds.get('url'),
'username': creds.get('username'),
'device_uuid': str(client.uuid) if hasattr(client, 'uuid') else None,
'account_pin': account_pin # Sauvegarder le PIN
}
except Exception as ex:
logger.warning(f"export_credentials a échoué: {ex}")
session_data = {'url': getattr(client, 'url', None)}
save_session_to_db(session_data)
flash('Connexion PRONOTE réussie !', 'success')
return redirect(url_for('pronote.index'))
else:
flash('Échec de la connexion PRONOTE.', 'danger')
except Exception as e:
flash(f'Erreur de connexion: {str(e)}', 'danger')
return redirect(url_for('pronote.connect'))
return render_template('pronote/connect.html')
@pronote_bp.route('/planning')
@pronote_bp.route('/planning_index')
@login_required
def planning():
"""Page principale des plannings."""
teaching_rooms = Room.query.filter(Room.room_type.has(is_teaching=True)).order_by(
Room.building_id, Room.name
).all()
return render_template('pronote/planning_index.html', rooms=teaching_rooms)
@pronote_bp.route('/rooms')
@login_required
def rooms():
"""Liste des salles."""
rooms_list = Room.query.order_by(Room.name).all()
return render_template('pronote/rooms.html', rooms=rooms_list)
@pronote_bp.route('/sync-salles', methods=['POST'])
@login_required
def sync_salles():
"""Synchroniser les salles depuis Pronote."""
from app_new.lib_ext.pronote_client import get_client, get_all_salles
client = get_client()
if not client:
flash('Non connecté à Pronote.', 'warning')
return redirect(url_for('pronote.connect'))
try:
salles_data = get_all_salles(client)
created = 0
for salle in salles_data:
salle_nom = salle.get('L', '')
if not Room.query.filter_by(name=salle_nom).first():
from app_new.core.models.college import Building, RoomType
building = Building.query.filter_by(name='Collège').first()
if not building:
building = Building(name='Collège', description='Bâtiment principal')
db.session.add(building)
db.session.commit()
cours_type = RoomType.query.filter_by(name='cours').first()
if not cours_type:
cours_type = RoomType(name='cours', is_teaching=True)
db.session.add(cours_type)
db.session.commit()
room = Room(name=salle_nom, code=salle_nom, floor=0, building_id=building.id, room_type_id=cours_type.id)
db.session.add(room)
created += 1
db.session.commit()
flash(f'{created} nouvelles salles synchronisées depuis Pronote.', 'success')
except Exception as e:
flash(f'Erreur synchronisation: {str(e)}', 'danger')
return redirect(url_for('pronote.rooms'))
@pronote_bp.route('/planning/<int:room_id>')
@login_required
def room_planning(room_id):
"""Emploi du temps d'une salle spécifique."""
from app_new.lib_ext.pronote_client import get_client
room = Room.query.get_or_404(room_id)
week_start = request.args.get("week", date.today().isoformat())
try:
monday = date.fromisoformat(week_start)
if monday.weekday() != 0:
monday = monday - timedelta(days=monday.weekday())
except:
monday = date.today() - timedelta(days=date.today().weekday())
week_end = monday + timedelta(days=6)
# Vérifier la connexion PRONOTE
client = get_client()
pronote_connected = client is not None
# La page ne consulte pas Pronote pour calculer l'occupation : elle
# affiche uniquement la vue interne résolue. Pronote est une source
# facultative, déclenchée par l'import explicite ci-dessous.
pronote_by_day = {}
# Récupérer les cours importés depuis la base de données
# Pronote reste une source facultative ; l'affichage des créneaux internes
# passe par le service de résolution commun pour masquer les créneaux
# supplantés/désactivés et conserver les protections manuelles.
from app_new.core.services.room_planning import active_room_schedules
db_schedules = active_room_schedules(room_id)
db_schedules = [s for s in db_schedules if s.week_start == monday]
db_by_day = {}
for schedule in db_schedules:
if schedule.day_of_week not in db_by_day:
db_by_day[schedule.day_of_week] = []
db_by_day[schedule.day_of_week].append({
'start_time': schedule.start_time.strftime('%H:%M') if schedule.start_time else '',
'end_time': schedule.end_time.strftime('%H:%M') if schedule.end_time else '',
'subject': schedule.subject or '',
'teacher': schedule.teacher or '',
'class_name': schedule.class_name or '',
'source': schedule.source,
'protected': schedule.protected_from_sync,
'conflict_note': schedule.conflict_note,
'last_synced_at': schedule.last_synced_at,
})
last_sync_at = max((item['last_synced_at'] for values in db_by_day.values() for item in values
if item['last_synced_at']), default=None)
return render_template('pronote/room_planning.html', room=room, week_start=monday, week_end=week_end,
prev_week=monday - timedelta(days=7), next_week=monday + timedelta(days=7),
today=date.today(), pronote_connected=pronote_connected, pronote_by_day=pronote_by_day,
db_by_day=db_by_day, last_sync_at=last_sync_at, timedelta=timedelta)
@pronote_bp.route('/planning/<int:room_id>/import', methods=['POST'])
@login_required
def import_room_planning(room_id):
"""Importer l'emploi du temps d'une salle depuis Pronote."""
from app_new.lib_ext.pronote_client import get_client, get_lessons_for_room
from app_new.core.services.room_planning import normalize_pronote_lessons, sync_pronote_schedules
room = Room.query.get_or_404(room_id)
client = get_client()
if not client:
flash('Pronote nest pas connecté : le planning interne reste disponible.', 'warning')
return redirect(url_for('pronote.room_planning', room_id=room_id))
week_value = request.form.get('week') or request.args.get('week') or date.today().isoformat()
try:
monday = date.fromisoformat(week_value)
monday -= timedelta(days=monday.weekday())
except ValueError:
monday = date.today() - timedelta(days=date.today().weekday())
try:
lessons = get_lessons_for_room(client, room.name, monday, monday + timedelta(days=6), strict=True)
entries = normalize_pronote_lessons(lessons, monday)
sync_pronote_schedules(room.id, entries)
flash(f'{len(entries)} créneaux Pronote synchronisés. Les protections locales ont été conservées.', 'success')
except Exception:
db.session.rollback()
logger.exception('Synchronisation Pronote impossible pour la salle %s', room.id)
flash('Pronote est momentanément indisponible. Les derniers créneaux connus sont conservés.', 'warning')
return redirect(url_for('pronote.room_planning', room_id=room_id, week=monday.isoformat()))
@pronote_bp.route('/disconnect', methods=['POST'])
@login_required
def disconnect():
"""Déconnexion de Pronote."""
session = PronoteSession.get()
if session:
session.session_token = None
db.session.commit()
flash('Déconnexion Pronote réussie.', 'info')
return redirect(url_for('pronote.index'))
@pronote_bp.route('/personnels')
@login_required
def personnels():
"""Liste des personnels depuis Pronote."""
from app_new.lib_ext.pronote_client import get_client
client = get_client()
if not client:
flash('Non connecté à Pronote.', 'warning')
return redirect(url_for('pronote.connect'))
try:
personnels = [{'name': 'Simulation Personnel 1', 'role': 'Admin'}, {'name': 'Simulation Personnel 2', 'role': 'Tech'}]
return render_template('pronote/personnels.html', personnels=personnels)
except Exception as e:
flash(f'Erreur: {str(e)}', 'danger')
return redirect(url_for('pronote.index'))
@pronote_bp.route('/professeurs')
@login_required
def professeurs():
"""Liste des professeurs depuis Pronote."""
from app_new.lib_ext.pronote_client import get_client
client = get_client()
if not client:
flash('Non connecté à Pronote.', 'warning')
return redirect(url_for('pronote.connect'))
try:
profs = [{'name': 'Simulation Prof 1', 'matiere': 'Maths'}, {'name': 'Simulation Prof 2', 'matiere': 'Physique'}]
return render_template('pronote/professeurs.html', profs=profs)
except Exception as e:
flash(f'Erreur: {str(e)}', 'danger')
return redirect(url_for('pronote.index'))