""" 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/') @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//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 n’est 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, week_start=monday) 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'))