""" Documents Routes - GMAO Collège Gestion des documents attachés aux interventions et équipements """ from flask import Blueprint, render_template, redirect, url_for, request, flash, current_app from flask_login import login_required, current_user from werkzeug.utils import secure_filename from datetime import datetime, timezone import os from uuid import uuid4 from app_new.extensions import db from ..core.models.maintenance import InterventionDocument from .service import list_documents, get_document, can_read_source documents_bp = Blueprint('documents', __name__, url_prefix='/documents') ALLOWED_EXTENSIONS = {'pdf', 'png', 'jpg', 'jpeg', 'gif', 'doc', 'docx', 'xls', 'xlsx'} def allowed_file(filename): return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS DOCUMENT_TYPES = { 'notice': 'Notice / manuel', 'facture': 'Facture', 'devis': 'Devis', 'rapport': 'Rapport / compte rendu', 'certificat': 'Certificat / contrôle', 'photo': 'Photo', 'plan': 'Plan', 'autre': 'Autre', } @documents_bp.route('/') @login_required def index(): """Centre documentaire global, agrégé sans recopier les fichiers.""" from datetime import date date_from = None date_to = None try: date_from = date.fromisoformat(request.args.get('date_from')) if request.args.get('date_from') else None date_to = date.fromisoformat(request.args.get('date_to')) if request.args.get('date_to') else None except ValueError: flash('La période de recherche est invalide.', 'warning') all_results = list_documents( query=request.args.get('q', ''), source=request.args.get('source') or None, document_type=request.args.get('document_type') or None, date_from=date_from, date_to=date_to, user=current_user, ) page = max(request.args.get('page', 1, type=int), 1) per_page = 25 total = len(all_results) results = all_results[(page - 1) * per_page: page * per_page] return render_template( 'documents/index.html', results=results, page=page, total=total, per_page=per_page, document_types=DOCUMENT_TYPES, filters=request.args, ) @documents_bp.route('/fds') @login_required def fds(): """Vue rapide des fiches de données de sécurité.""" results = list_documents( query=request.args.get('q', ''), source='product', user=current_user, fds_only=True, ) return render_template('documents/fds.html', results=results, filters=request.args) @documents_bp.route('/download//') @login_required def download(source, document_id): """Téléchargement contrôlé, sans exposer le chemin de stockage.""" from flask import send_file document = get_document(source, document_id) if not document or not can_read_source(source, current_user): from flask import abort abort(403) filepath = os.path.realpath(document.filepath or '') upload_root = os.path.realpath(current_app.config.get('UPLOAD_FOLDER', 'uploads')) if not filepath or not os.path.isfile(filepath) or not (filepath == upload_root or filepath.startswith(upload_root + os.sep)): current_app.logger.warning('Document physique introuvable ou hors racine: %s/%s', source, document_id) flash('Le fichier documentaire est momentanément indisponible.', 'error') return redirect(url_for('documents.index')) return send_file(filepath, as_attachment=True, download_name=os.path.basename(document.filename or filepath)) @documents_bp.route('/upload/intervention/', methods=['POST']) @login_required def upload_intervention(intervention_id): """Upload un document pour une intervention.""" from ..core.models.maintenance import Intervention Intervention.query.get_or_404(intervention_id) if 'file' not in request.files: flash('Aucun fichier sélectionné.', 'error') return redirect(url_for('interventions.detail', id=intervention_id)) file = request.files['file'] if file.filename == '': flash('Aucun fichier sélectionné.', 'error') return redirect(url_for('interventions.detail', id=intervention_id)) if file and allowed_file(file.filename): filename = secure_filename(file.filename) unique_filename = f"{uuid4().hex}_{filename}" upload_dir = os.path.join(current_app.config.get('UPLOAD_FOLDER', 'uploads'), 'interventions') os.makedirs(upload_dir, exist_ok=True) filepath = os.path.join(upload_dir, unique_filename) file.save(filepath) document = InterventionDocument( intervention_id=intervention_id, filename=unique_filename, filepath=filepath, description=request.form.get('description', ''), document_type=request.form.get('document_type') or 'autre', uploaded_by_id=current_user.id ) db.session.add(document) db.session.commit() flash('Document uploadé avec succès.', 'success') else: flash('Type de fichier non autorisé.', 'error') return redirect(url_for('interventions.detail', id=intervention_id)) @documents_bp.route('/upload/equipment/', methods=['POST']) @login_required def upload_equipment(equipment_id): """Upload un document pour un équipement.""" from ..core.models.equipment import Equipment, EquipmentDocument Equipment.query.get_or_404(equipment_id) if 'file' not in request.files: flash('Aucun fichier sélectionné.', 'error') return redirect(url_for('equipments.detail', id=equipment_id)) file = request.files['file'] if file.filename == '': flash('Aucun fichier sélectionné.', 'error') return redirect(url_for('equipments.detail', id=equipment_id)) if file and allowed_file(file.filename): filename = secure_filename(file.filename) unique_filename = f"{uuid4().hex}_{filename}" upload_dir = os.path.join(current_app.config.get('UPLOAD_FOLDER', 'uploads'), 'equipments') os.makedirs(upload_dir, exist_ok=True) filepath = os.path.join(upload_dir, unique_filename) file.save(filepath) document = EquipmentDocument( equipment_id=equipment_id, filename=unique_filename, filepath=filepath, document_type=request.form.get('document_type') or 'autre', uploaded_by_id=current_user.id ) db.session.add(document) db.session.commit() flash('Document uploadé avec succès.', 'success') else: flash('Type de fichier non autorisé.', 'error') return redirect(url_for('equipments.detail', id=equipment_id)) @documents_bp.route('/intervention/delete/', methods=['POST']) @login_required def delete_intervention_document(doc_id): """Supprimer un document d'intervention.""" document = InterventionDocument.query.get_or_404(doc_id) intervention_id = document.intervention_id # Supprimer le fichier physique if document.filepath and os.path.exists(document.filepath): os.remove(document.filepath) db.session.delete(document) db.session.commit() flash('Document supprimé.', 'success') return redirect(url_for('interventions.detail', id=intervention_id)) @documents_bp.route('/intervention//edit', methods=['POST']) @login_required def edit_intervention(doc_id): """Modifier uniquement la description d'un document d'intervention.""" document = InterventionDocument.query.get_or_404(doc_id) document.description = (request.form.get('description') or '').strip() or None db.session.commit() flash('Description du document mise à jour.', 'success') return redirect(url_for('interventions.detail', id=document.intervention_id)) @documents_bp.route('/intervention/download/') @login_required def download_intervention_document(doc_id): """Télécharger un document d'intervention.""" from flask import send_file document = InterventionDocument.query.get_or_404(doc_id) if document.filepath and os.path.exists(document.filepath): return send_file(document.filepath, as_attachment=True, download_name=document.filename) flash('Fichier non trouvé.', 'error') return redirect(url_for('interventions.detail', id=document.intervention_id)) @documents_bp.route('/equipment/delete/', methods=['POST']) @login_required def delete_equipment_document(doc_id): """Supprimer un document d'équipement.""" from ..core.models.equipment import EquipmentDocument document = EquipmentDocument.query.get_or_404(doc_id) equipment_id = document.equipment_id # Supprimer le fichier physique if document.filepath and os.path.exists(document.filepath): os.remove(document.filepath) db.session.delete(document) db.session.commit() flash('Document supprimé.', 'success') return redirect(url_for('equipments.detail', id=equipment_id)) @documents_bp.route('/equipment/download/') @login_required def download_equipment_document(doc_id): """Télécharger un document d'équipement.""" from flask import send_file from ..core.models.equipment import EquipmentDocument document = EquipmentDocument.query.get_or_404(doc_id) if document.filepath and os.path.exists(document.filepath): return send_file(document.filepath, as_attachment=True, download_name=document.filename) flash('Fichier non trouvé.', 'error') return redirect(url_for('equipments.detail', id=document.equipment_id))