import os from flask import ( Blueprint, render_template, redirect, url_for, flash, request, jsonify, send_file, abort, ) from flask_login import login_required, current_user from flask_wtf import FlaskForm from flask_wtf.file import FileField, FileRequired from wtforms import StringField, TextAreaField, SubmitField, HiddenField from wtforms.validators import DataRequired, Length from werkzeug.utils import secure_filename from services.document_service import ( add_document, add_version, get_document, get_project_documents, get_document_version, get_latest_version, register_download, delete_document, ) from services.project_service import get_project from services.schema_service import get_schema_document_types from utils.security import permission_required # Definir Blueprint documents_bp = Blueprint("documents", __name__, url_prefix="/documents") # Formularios class DocumentUploadForm(FlaskForm): """Formulario para subir documento.""" nombre = StringField( "Nombre del documento", validators=[DataRequired(), Length(1, 100)] ) description = TextAreaField("Descripción", validators=[Length(0, 500)]) file = FileField("Archivo", validators=[FileRequired()]) submit = SubmitField("Subir documento") class DocumentVersionForm(FlaskForm): """Formulario para nueva versión de documento.""" description = TextAreaField( "Descripción de la versión", validators=[Length(0, 500)] ) file = FileField("Archivo", validators=[FileRequired()]) document_id = HiddenField("ID de documento", validators=[DataRequired()]) submit = SubmitField("Subir nueva versión") # Rutas @documents_bp.route("/") @login_required def list(project_id): """Listar documentos de un proyecto.""" project = get_project(project_id) if not project: flash("Proyecto no encontrado.", "danger") return redirect(url_for("projects.list")) documents = get_project_documents(project_id) return render_template("documents/list.html", project=project, documents=documents) @documents_bp.route("//upload", methods=["GET", "POST"]) @login_required @permission_required(1000) # Nivel mínimo para subir documentos def upload(project_id): """Subir un nuevo documento.""" project = get_project(project_id) if not project: flash("Proyecto no encontrado.", "danger") return redirect(url_for("projects.list")) # Verificar que el proyecto esté activo if project["estado"] != "activo": flash("No se pueden añadir documentos a un proyecto inactivo.", "warning") return redirect(url_for("projects.view", project_id=project_id)) form = DocumentUploadForm() if form.validate_on_submit(): # Añadir documento success, message, document_id = add_document( project_id, {"nombre": form.nombre.data, "description": form.description.data}, form.file.data, current_user.username, ) if success: flash(message, "success") return redirect( url_for( "documents.versions", project_id=project_id, document_id=document_id ) ) else: flash(message, "danger") return render_template("documents/upload.html", form=form, project=project) @documents_bp.route("//") @login_required def versions(project_id, document_id): """Ver versiones de un documento.""" project = get_project(project_id) if not project: flash("Proyecto no encontrado.", "danger") return redirect(url_for("projects.list")) document = get_document(project_id, document_id) if not document: flash("Documento no encontrado.", "danger") return redirect(url_for("projects.view", project_id=project_id)) form = DocumentVersionForm() form.document_id.data = document_id return render_template( "documents/versions.html", project=project, document=document, form=form ) @documents_bp.route("///upload", methods=["POST"]) @login_required @permission_required(1000) # Nivel mínimo para subir versiones def upload_version(project_id, document_id): """Subir una nueva versión de documento.""" project = get_project(project_id) if not project: flash("Proyecto no encontrado.", "danger") return redirect(url_for("projects.list")) # Verificar que el proyecto esté activo if project["estado"] != "activo": flash("No se pueden añadir versiones a un proyecto inactivo.", "warning") return redirect(url_for("projects.view", project_id=project_id)) document = get_document(project_id, document_id) if not document: flash("Documento no encontrado.", "danger") return redirect(url_for("projects.view", project_id=project_id)) form = DocumentVersionForm() if form.validate_on_submit(): # Añadir versión success, message, version = add_version( project_id, document_id, {"description": form.description.data}, form.file.data, current_user.username, ) if success: flash(message, "success") else: flash(message, "danger") else: for field, errors in form.errors.items(): for error in errors: flash(f"{getattr(form, field).label.text}: {error}", "danger") return redirect( url_for("documents.versions", project_id=project_id, document_id=document_id) ) @documents_bp.route("///download/") @login_required def download(project_id, document_id, version): """Descargar una versión específica de un documento.""" project = get_project(project_id) if not project: flash("Proyecto no encontrado.", "danger") return redirect(url_for("projects.list")) # Obtener versión solicitada version_meta, file_path = get_document_version(project_id, document_id, version) if not version_meta or not file_path: flash("Versión de documento no encontrada.", "danger") return redirect(url_for("projects.view", project_id=project_id)) # Registrar descarga register_download(project_id, document_id, version, current_user.username) # Enviar archivo try: return send_file( file_path, mimetype=version_meta["mime_type"], as_attachment=True, download_name=os.path.basename(file_path), ) except Exception as e: flash(f"Error al descargar el archivo: {str(e)}", "danger") return redirect( url_for( "documents.versions", project_id=project_id, document_id=document_id ) ) @documents_bp.route("///latest") @login_required def download_latest(project_id, document_id): """Descargar la última versión de un documento.""" project = get_project(project_id) if not project: flash("Proyecto no encontrado.", "danger") return redirect(url_for("projects.list")) # Obtener última versión version_meta, file_path = get_latest_version(project_id, document_id) if not version_meta or not file_path: flash("Documento no encontrado.", "danger") return redirect(url_for("projects.view", project_id=project_id)) # Registrar descarga register_download( project_id, document_id, version_meta["version"], current_user.username ) # Enviar archivo try: return send_file( file_path, mimetype=version_meta["mime_type"], as_attachment=True, download_name=os.path.basename(file_path), ) except Exception as e: flash(f"Error al descargar el archivo: {str(e)}", "danger") return redirect( url_for( "documents.versions", project_id=project_id, document_id=document_id ) ) @documents_bp.route("///delete", methods=["POST"]) @login_required @permission_required(9000) # Nivel alto para eliminar documentos def delete(project_id, document_id): """Eliminar un documento.""" project = get_project(project_id) if not project: flash("Proyecto no encontrado.", "danger") return redirect(url_for("projects.list")) success, message = delete_document(project_id, document_id) if success: flash(message, "success") else: flash(message, "danger") return redirect(url_for("projects.view", project_id=project_id)) @documents_bp.route("//export") @login_required def export(project_id): """Exportar documentos de un proyecto.""" project = get_project(project_id) if not project: flash("Proyecto no encontrado.", "danger") return redirect(url_for("projects.list")) documents = get_project_documents(project_id) return render_template( "documents/export.html", project=project, documents=documents ) @documents_bp.route("//api/list") @login_required def api_list(project_id): """API para listar documentos de un proyecto.""" documents = get_project_documents(project_id) return jsonify(documents)