2026-08-14 18:02:25 +02:00
|
|
|
"""
|
|
|
|
|
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
|
2026-08-15 00:56:36 +02:00
|
|
|
from uuid import uuid4
|
2026-08-14 18:02:25 +02:00
|
|
|
from app_new.extensions import db
|
|
|
|
|
from ..core.models.maintenance import InterventionDocument
|
2026-08-23 00:44:21 +02:00
|
|
|
from .service import list_documents, get_document, can_read_source
|
2026-08-14 18:02:25 +02:00
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
2026-08-23 00:44:21 +02:00
|
|
|
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/<source>/<int:document_id>')
|
|
|
|
|
@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))
|
|
|
|
|
|
|
|
|
|
|
2026-08-14 18:02:25 +02:00
|
|
|
@documents_bp.route('/upload/intervention/<int:intervention_id>', methods=['POST'])
|
|
|
|
|
@login_required
|
|
|
|
|
def upload_intervention(intervention_id):
|
|
|
|
|
"""Upload un document pour une intervention."""
|
2026-08-15 00:56:36 +02:00
|
|
|
from ..core.models.maintenance import Intervention
|
|
|
|
|
Intervention.query.get_or_404(intervention_id)
|
2026-08-14 18:02:25 +02:00
|
|
|
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)
|
2026-08-15 00:56:36 +02:00
|
|
|
unique_filename = f"{uuid4().hex}_{filename}"
|
2026-08-14 18:02:25 +02:00
|
|
|
|
|
|
|
|
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', ''),
|
2026-08-23 00:44:21 +02:00
|
|
|
document_type=request.form.get('document_type') or 'autre',
|
2026-08-14 18:02:25 +02:00
|
|
|
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/<int:equipment_id>', methods=['POST'])
|
|
|
|
|
@login_required
|
|
|
|
|
def upload_equipment(equipment_id):
|
|
|
|
|
"""Upload un document pour un équipement."""
|
2026-08-15 00:56:36 +02:00
|
|
|
from ..core.models.equipment import Equipment, EquipmentDocument
|
|
|
|
|
Equipment.query.get_or_404(equipment_id)
|
2026-08-14 18:02:25 +02:00
|
|
|
|
|
|
|
|
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)
|
2026-08-15 00:56:36 +02:00
|
|
|
unique_filename = f"{uuid4().hex}_{filename}"
|
2026-08-14 18:02:25 +02:00
|
|
|
|
|
|
|
|
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,
|
2026-08-23 00:44:21 +02:00
|
|
|
document_type=request.form.get('document_type') or 'autre',
|
2026-08-14 18:02:25 +02:00
|
|
|
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/<int:doc_id>', 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))
|
|
|
|
|
|
|
|
|
|
|
2026-08-15 00:47:55 +02:00
|
|
|
@documents_bp.route('/intervention/<int:doc_id>/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))
|
|
|
|
|
|
|
|
|
|
|
2026-08-14 18:02:25 +02:00
|
|
|
@documents_bp.route('/intervention/download/<int:doc_id>')
|
|
|
|
|
@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/<int:doc_id>', 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/<int:doc_id>')
|
|
|
|
|
@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')
|
2026-08-15 00:47:55 +02:00
|
|
|
return redirect(url_for('equipments.detail', id=document.equipment_id))
|