Flask

Flask

Tutorial Flask: Resultados de IA con Jinja en Flask

Muestra resultados de IA con Jinja y sintaxis HTML en Flask, muestra resultados de base de datos y de integraciones con IA LLMs.

Aprende Flask GRATIS y certifícate

Introducción a Jinja2 en HTML y primeros pasos sobre Flask

Jinja2 es un motor de plantillas para Python que facilita la creación de contenido HTML dinámico. En combinación con Flask, un microframework web, permite desarrollar aplicaciones web de manera eficiente, separando la lógica de negocio de la presentación.

Para comenzar, es necesario instalar Flask. Asegúrate de tener una versión actualizada de Python 3.13 y ejecuta el siguiente comando:

pip install flask

Una vez instalado, creamos un archivo llamado app.py que será el punto de entrada de nuestra aplicación:

from flask import Flask, render_template

app = Flask(__name__)

@app.route('/')
def inicio():
    return render_template('index.html', titulo='Inicio')

if __name__ == '__main__':
    app.run(debug=True)

En este código, definimos una ruta básica '/' que renderiza la plantilla index.html y le pasamos una variable titulo. La función render_template utiliza Jinja2 para procesar la plantilla.

A continuación, creamos el directorio templates en el mismo nivel que app.py y dentro de él, el archivo index.html:

<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <title>{{ titulo }}</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
    <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css" rel="stylesheet">
</head>
<body>
    <div class="container">
        <h1 class="mt-5">{{ titulo }}</h1>
        <p class="lead">Bienvenido a nuestra aplicación Flask con Jinja2.</p>
        <button class="btn btn-primary" onclick="alert('¡Hola desde Flask con Jinja2!')">
            <i class="fas fa-thumbs-up"></i> Me gusta
        </button>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

En esta plantilla, utilizamos la sintaxis de Jinja2 {{ variable }} para insertar el valor de titulo. Además, incorporamos Bootstrap CSS y Font Awesome mediante enlaces CDN para estilizar nuestra página y utilizar iconos.

Al ejecutar la aplicación con python app.py, Flask inicia un servidor local y muestra un mensaje indicando que la aplicación está en ejecución en http://127.0.0.1:5000/. Al visitar esta dirección en un navegador, veremos nuestra página renderizada con el título y contenido especificados.

Es importante destacar que Jinja2 admite no solo variables, sino también expresiones y estructuras de control. Por ejemplo, podemos utilizar:

<p>El resultado es: {{ 2 + 2 }}</p>

O iterar sobre una lista:

<ul>
{% for elemento in lista %}
    <li>{{ elemento }}</li>
{% endfor %}
</ul>

Para pasar datos más complejos desde Flask, modificamos nuestra ruta en app.py:

@app.route('/usuarios')
def usuarios():
    nombres = ['Ana', 'Luis', 'María', 'Juan']
    return render_template('usuarios.html', usuarios=nombres)

Y en usuarios.html:

<h2>Lista de Usuarios</h2>
<ul>
    {% for usuario in usuarios %}
        <li>{{ usuario }}</li>
    {% endfor %}
</ul>

De esta manera, Jinja2 nos permite renderizar contenido dinámico basado en datos proporcionados desde nuestras funciones de vista en Flask.

Para estructurar mejor nuestra aplicación, podemos crear un directorio static para archivos estáticos como hojas de estilo personalizadas o scripts JavaScript. Por ejemplo, si añadimos styles.css en static/css/styles.css, lo enlazamos en nuestra plantilla dentro de la etiqueta head:

<head>
    <meta charset="UTF-8">
    <title>{{ titulo }}</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
    <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css" rel="stylesheet">
    <link href="{{ url_for('static', filename='css/styles.css') }}" rel="stylesheet">
</head>

La función url_for genera la URL correcta para los recursos estáticos, lo que es esencial para mantener la compatibilidad en diferentes entornos de desarrollo y producción.

Control de flujo y filtros

Jinja2 proporciona estructuras de control de flujo que permiten manipular la lógica dentro de las plantillas, haciendo que el contenido HTML sea más dinámico y adaptativo. Estas estructuras incluyen sentencias como if, for, y macro, que facilitan la interacción con los datos proporcionados desde Flask.

Sentencia if

La sentencia if se utiliza para introducir condiciones en las plantillas. Permite mostrar o ocultar secciones del HTML basándose en variables o expresiones. Por ejemplo:

{% if usuario_autenticado %}
    <p>Bienvenido, {{ nombre_usuario }}.</p>
{% else %}
    <p>Por favor, inicia sesión para continuar.</p>
{% endif %}

En este fragmento, si la variable usuario_autenticado es verdadera, se muestra un mensaje de bienvenida; de lo contrario, se invita al usuario a iniciar sesión.

Sentencia for

La sentencia for permite iterar sobre listas o diccionarios y generar contenido repetitivo. Por ejemplo, para listar productos:

<ul>
    {% for producto in lista_productos %}
        <li>{{ producto.nombre }} - {{ producto.precio }}€</li>
    {% endfor %}
</ul>

En este caso, por cada producto en lista_productos, se crea un elemento de lista mostrando el nombre y el precio.

Uso de loop

Dentro de un bucle for, Jinja2 proporciona la variable especial loop, que ofrece información adicional sobre la iteración actual:

<ul>
    {% for item in items %}
        <li>{{ loop.index }}. {{ item }}</li>
    {% endfor %}
</ul>

Aquí, loop.index muestra el número de iteración actual, comenzando desde 1. Esto es útil para numerar elementos o aplicar estilos específicos en ciertas posiciones.

Control de flujo avanzado

Además de las sentencias básicas, Jinja2 soporta estructuras como elif y else en los condicionales:

{% if puntuacion >= 90 %}
    <p>Excelente</p>
{% elif puntuacion >= 70 %}
    <p>Bueno</p>
{% else %}
    <p>Necesita mejorar</p>
{% endif %}

Este control granular permite crear plantillas altamente dinámicas que reaccionan a diferentes estados de la aplicación.

Filtros

Los filtros son funciones que transforman el valor de una variable. Similar a los pipes de Angular, se aplican utilizando el símbolo | seguido del nombre del filtro. 

Por ejemplo:

<p>Fecha actual: {{ fecha_actual | date('d/m/Y') }}</p>

En este caso, el filtro date formatea el objeto fecha_actual según el formato especificado.

Algunos filtros incorporados en Jinja2 incluyen:

  • upper: convierte una cadena a mayúsculas.
  <p>{{ nombre | upper }}</p>
  • lower: convierte una cadena a minúsculas.
  <p>{{ nombre | lower }}</p>
  • length: devuelve la longitud de una secuencia.
  <p>Número de elementos: {{ lista | length }}</p>
  • join: combina elementos de una lista en una cadena.
  <p>{{ lista_nombres | join(', ') }}</p>

Es posible crear filtros personalizados en Flask para usarlos en las plantillas Jinja2. Para ello, definimos una función y la registramos en el entorno de Jinja2:

def capitalizar_palabras(s):
    return s.title()

app.jinja_env.filters['capitalizar'] = capitalizar_palabras

Luego, en la plantilla:

<p>{{ titulo | capitalizar }}</p>

Este filtro capitalizar convierte el texto de titulo en formato de título, capitalizando la primera letra de cada palabra.

Algunos filtros aceptan argumentos adicionales. Por ejemplo, el filtro replace reemplaza partes de una cadena:

<p>{{ mensaje | replace('mundo', 'usuario') }}</p>

Si mensaje es "Hola, mundo", el resultado será "Hola, usuario".

Por defecto, Jinja2 escapa las variables para prevenir ataques de tipo XSS. Sin embargo, si estamos seguros de que el contenido es seguro, podemos utilizar el filtro safe:

<p>{{ contenido_html | safe }}</p>

Se debe usar este filtro con precaución, asegurando que el contenido no proviene de fuentes no confiables.

Uso de Bootstrap y Font awesome

Bootstrap es un framework de CSS que facilita el diseño responsive y estilizado de sitios web mediante un sistema de rejilla, componentes predefinidos y utilidades.

Por otro lado, Font awesome es una biblioteca de iconos vectoriales escalables que se pueden personalizar fácilmente con CSS para usarse en sitios web y aplicaciones.

Combinando control de flujo y filtros en Jinja, podemos crear interfaces más interactivas. Por ejemplo, mostrar iconos de Font Awesome basados en una condición:

{% if estado == 'activo' %}
    <i class="fas fa-check-circle text-success"></i>
{% else %}
    <i class="fas fa-times-circle text-danger"></i>
{% endif %}

Además, podemos utilizar Bootstrap CSS para estilizar listas generadas dinámicamente:

<ul class="list-group">
    {% for tarea in tareas %}
        <li class="list-group-item d-flex justify-content-between align-items-center">
            {{ tarea.nombre }}
            {% if tarea.completada %}
                <span class="badge bg-success">Completada</span>
            {% else %}
                <span class="badge bg-secondary">Pendiente</span>
            {% endif %}
        </li>
    {% endfor %}
</ul>

En este código, utilizamos estructuras de control para determinar qué etiqueta y estilo aplicar a cada tarea en función de si está completada o no.

Macros y bloques

Las macros permiten reutilizar código dentro de las plantillas. Podemos definir una macro para un botón personalizado:

{% macro boton_enlace(url, texto, icono) %}
    <a href="{{ url }}" class="btn btn-primary">
        <i class="{{ icono }}"></i> {{ texto }}
    </a>
{% endmacro %}

Luego, podemos utilizar la macro en la plantilla:

{{ boton_enlace('/editar', 'Editar', 'fas fa-edit') }}

Esto simplifica la plantilla y mejora la modularidad del código.

Herencia de plantillas e inclusión de fragmentos en Jinja2

La herencia de plantillas en Jinja2 permite definir una estructura base para las páginas web y reutilizarla en diferentes plantillas, promoviendo la modularidad y reduciendo la duplicación de código. Además, la inclusión de fragmentos facilita la inserción de componentes comunes como menús de navegación, pies de página o interfaces reutilizables.

Para comenzar con la herencia de plantillas, creamos una plantilla base que servirá como esqueleto para todas las páginas de nuestra aplicación Flask. Por convención, esta plantilla se suele llamar base.html y se ubica en el directorio templates:

<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <title>{% block titulo %}Aplicación Flask{% endblock %}</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
    <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css" rel="stylesheet">
    <link href="{{ url_for('static', filename='css/styles.css') }}" rel="stylesheet">
</head>
<body>
    <!-- Incluimos el menú de navegación -->
    {% include 'navbar.html' %}
    <div class="container">
        {% block contenido %}
        <!-- Contenido específico de cada página -->
        {% endblock %}
    </div>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

En esta plantilla, utilizamos bloques definidos con {% block %} y {% endblock %} para marcar secciones que las plantillas hijas pueden sobrescribir. Los bloques titulo y contenido permiten personalizar el título de la página y el contenido principal respectivamente.

Para crear una página específica, como una página de inicio, creamos una nueva plantilla que extiende de base.html. Por ejemplo, index.html:

{% extends 'base.html' %}

{% block titulo %}Inicio{% endblock %}

{% block contenido %}
    <h1 class="mt-5">Bienvenido a la aplicación Flask</h1>
    <p class="lead">Esta es la página de inicio. Utilizamos la <strong>herencia de plantillas</strong> de Jinja2 para estructurar nuestro sitio.</p>
{% endblock %}

La directiva {% extends 'base.html' %} indica que esta plantilla heredará de base.html. Dentro de los bloques, sobrescribimos el contenido específico para esta página.

Podemos definir múltiples bloques en la plantilla base.html para tener mayor control sobre la estructura. Por ejemplo, añadimos un bloque para incluir scripts específicos de cada página:

<body>
    {% include 'navbar.html' %}
    <div class="container">
        {% block contenido %}
        {% endblock %}
    </div>
    {% block scripts %}
    <!-- Scripts específicos de cada plantilla -->
    {% endblock %}
    <!-- Scripts de Bootstrap -->
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>

En la plantilla hija, podemos añadir scripts adicionales dentro del bloque scripts:

{% extends 'base.html' %}

{% block titulo %}Página de Usuarios{% endblock %}

{% block contenido %}
    <h2>Usuarios Registrados</h2>
    <ul class="list-group">
        {% for usuario in usuarios %}
            <li class="list-group-item">{{ usuario.nombre }}</li>
        {% endfor %}
    </ul>
{% endblock %}

{% block scripts %}
    <script>
        console.log('Página de usuarios cargada.');
    </script>
{% endblock %}

La directiva {% include %} permite insertar fragmentos de código o componentes reutilizables en nuestras plantillas. Esto es útil para elementos comunes como barras de navegación, formularios o pies de página.

Creamos un archivo navbar.html en el directorio templates:

<nav class="navbar navbar-expand-lg navbar-light bg-light">
    <div class="container-fluid">
        <a class="navbar-brand" href="{{ url_for('inicio') }}">Mi Aplicación</a>
        <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarContenido" aria-controls="navbarContenido" aria-expanded="false" aria-label="Toggle navigation">
            <span class="navbar-toggler-icon"></span>
        </button>
        <div class="collapse navbar-collapse" id="navbarContenido">
            <ul class="navbar-nav me-auto mb-2 mb-lg-0">
                <li class="nav-item">
                    <a class="nav-link" href="{{ url_for('inicio') }}">Inicio</a>
                </li>
                <li class="nav-item">
                    <a class="nav-link" href="{{ url_for('usuarios') }}">Usuarios</a>
                </li>
            </ul>
        </div>
    </div>
</nav>

Este fragmento define una barra de navegación utilizando Bootstrap CSS. En la plantilla base, lo incluimos con {% include 'navbar.html' %}.

La inclusión de fragmentos permite mantener un código más limpio y modular. Si necesitamos actualizar el menú de navegación, solo modificamos navbar.html, y los cambios se reflejarán en todas las páginas que lo incluyen.

Los bloques pueden ser anidados, lo que ofrece mayor flexibilidad. Por ejemplo, en base.html, podemos tener:

<div class="container">
    {% block contenido %}
        <div class="row">
            {% block contenido_principal %}
            <!-- Contenido principal por defecto -->
            {% endblock %}
        </div>
    {% endblock %}
</div>

En una plantilla hija, podemos sobrescribir contenido_principal:

{% extends 'base.html' %}

{% block titulo %}Detalle de Usuario{% endblock %}

{% block contenido_principal %}
    <h2>Perfil de {{ usuario.nombre }}</h2>
    <p><strong>Email:</strong> {{ usuario.email }}</p>
    <p><strong>Registrado el:</strong> {{ usuario.fecha_registro | date('d/m/Y') }}</p>
{% endblock %}

Esto nos permite personalizar secciones específicas sin alterar la estructura general.

En ocasiones, es útil añadir contenido al bloque heredado en lugar de reemplazarlo completamente. Para ello, utilizamos {{ super() }}. Por ejemplo:

{% block scripts %}
    {{ super() }}
    <script src="{{ url_for('static', filename='js/mi_script.js') }}"></script>
{% endblock %}

Aquí, {{ super() }} incluye el contenido original del bloque scripts definido en la plantilla base, y añadimos scripts adicionales específicos de la plantilla hija.

Es aconsejable organizar las plantillas en subdirectorios para mejorar la mantenibilidad. Por ejemplo:

templates/
    base.html
    navbar.html
    usuarios/
        index.html
        detalle.html
    productos/
        lista.html
        formulario.html

De esta manera, agrupamos las plantillas relacionadas, facilitando su gestión en proyectos de mayor envergadura.

Podemos incluir fragmentos de forma condicional utilizando estructuras de control:

{% if usuario_autenticado %}
    {% include 'menu_usuario.html' %}
{% else %}
    {% include 'menu_invitado.html' %}
{% endif %}

Esto permite mostrar diferentes menús o componentes dependiendo del estado de la aplicación.

Los fragmentos incluidos pueden recibir variables del contexto actual. Por ejemplo, en navbar.html podemos usar:

<span class="navbar-text">
    {% if usuario_autenticado %}
        Bienvenido, {{ usuario.nombre }}
    {% else %}
        Invitado
    {% endif %}
</span>

No es necesario pasar explícitamente las variables; el fragmento tiene acceso al contexto de la plantilla que lo incluye.

Aunque la inclusión es útil para fragmentos estáticos, las macros son ideales para crear componentes reutilizables con parámetros. Por ejemplo, definimos una macro en un archivo macros.html:

{% macro mostrar_alerta(tipo, mensaje) %}
    <div class="alert alert-{{ tipo }} alert-dismissible fade show" role="alert">
        {{ mensaje }}
        <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Cerrar"></button>
    </div>
{% endmacro %}

En la plantilla donde queremos usarla, primero importamos las macros:

{% import 'macros.html' as ui %}

Luego, utilizamos la macro:

{{ ui.mostrar_alerta('success', 'Operación realizada con éxito.') }}

Esto simplifica la creación de elementos dinámicos y mantiene el código ordenado.

Para añadir comentarios en las plantillas que no se rendericen en el HTML final, utilizamos {# ... #}:

{# Este es un comentario que no aparecerá en el HTML generado #}

Es útil para incluir anotaciones o notas para otros desarrolladores sin afectar la salida.

En nuestra aplicación Flask, definimos rutas que renderizan las plantillas heredadas. Por ejemplo, en app.py:

from flask import Flask, render_template

app = Flask(__name__)

@app.route('/')
def inicio():
    return render_template('index.html')

@app.route('/usuarios')
def lista_usuarios():
    usuarios = obtener_usuarios()  # Función que obtiene los usuarios
    return render_template('usuarios/index.html', usuarios=usuarios)

@app.route('/usuarios/<int:id>')
def detalle_usuario(id):
    usuario = obtener_usuario_por_id(id)
    return render_template('usuarios/detalle.html', usuario=usuario)

Las funciones render_template buscan en el directorio templates las plantillas especificadas. Al utilizar herencia e inclusión, podemos construir páginas complejas de forma estructurada y eficiente.

Manejo de formularios en Jinja2 y Flask

El manejo de formularios es esencial en el desarrollo de aplicaciones web interactivas, permitiendo la comunicación entre el usuario y el servidor. 

En Flask, los formularios se gestionan mediante solicitudes POST y GET, y Jinja2 facilita su renderización en las plantillas. 

Vamos a crear una nueva aplicación Flask con un formulario de contacto que permita recibir mensajes y responder con una pantalla personalizada.

Crear app.py

Primero creamos la aplicación, creando un nuevo archivo app.py con un método para mostrar el formulario de contacto y recibir lo datos, y otro método para la pantalla de gracias como mensaje personalizado tras enviar el formulario:


from flask import Flask, render_template, request, redirect, url_for, session, abort
import secrets

app = Flask(__name__)
# Clave secreta para la sesión (requerida para CSRF manual)
app.config['SECRET_KEY'] = 'mi-super-clave-secreta'

@app.route('/contacto', methods=['GET', 'POST'])
def contacto():
    if request.method == 'POST':
        # 1. Recuperar el token CSRF enviado y el que guardamos en sesión
        token_enviado = request.form.get('csrf_token')
        token_sesion = session.pop('csrf_token', None)  # pop lo elimina tras leerlo

        # 2. Verificar que el token no falte y que coincida
        if not token_sesion or token_enviado != token_sesion:
            abort(400, description="Token CSRF inválido o ausente.")

        # 3. Obtener valores del formulario (el 'strip' elimina espacios al inicio y fin)
        nombre = request.form.get('nombre', '').strip()
        email = request.form.get('email', '').strip()
        mensaje = request.form.get('mensaje', '').strip()

        # 4. Validar datos
        errores = []
        if not nombre:
            errores.append("El nombre es obligatorio.")
        if not email or '@' not in email:
            errores.append("Debes introducir un correo electrónico válido.")
        if not mensaje:
            errores.append("El mensaje no puede estar vacío.")

        # 5. En caso de errores, recargar el formulario
        if errores:
            # Generar un nuevo token CSRF
            session['csrf_token'] = secrets.token_hex(16)
            return render_template(
                'contacto.html',
                errores=errores,
                csrf_token=session['csrf_token'],
                nombre=nombre,
                email=email,
                mensaje=mensaje
            )

        # 6. Si todo es correcto, podríamos guardar en BD, enviar un correo, etc.
        # Redirigimos a la página de agradecimiento junto con el nombre
        return redirect(url_for('gracias', nombre=nombre))

    # Si es GET, generamos un nuevo token CSRF para el formulario
    session['csrf_token'] = secrets.token_hex(16)
    return render_template('contacto.html', csrf_token=session['csrf_token'])

@app.route('/gracias')
def gracias():

    nombre = request.args.get('nombre', 'Usuario')
    return render_template('gracias.html', nombre=nombre)

if __name__ == '__main__':
    app.run(debug=True)

Este código de Python utiliza request.form para obtener los datos recibidos al enviar el formulario, los pasa a variables y los valida manualmente, otra opción es validarlos usando Pydantic.

Si hay errores los carga de nuevo en el formulario mostrándolo otra vez, para que el usuario sepa qué ha ido mal.

Si todo va bien se redirige a la pantalla de gracias, donde aparecerá un mensaje personalizado.

La implementación de un token CSRF añade una capa adicional de seguridad al formulario. Al generar un token único por sesión y verificarlo en cada solicitud POST, prevenimos ataques maliciosos que intenten enviar solicitudes automatizadas.

Creación de contacto.html

Creamos la carpeta templates, y dentro el archivo contacto.html con un formulario con bootstrap para facilitar la usabilidad:

<!-- templates/contacto.html -->
<!DOCTYPE html>
<html lang="es">
<head>
  <meta charset="UTF-8">
  <title>Contacto</title>
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
  <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.0/css/all.min.css" rel="stylesheet">
</head>
<body class="bg-light">

<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
  <div class="container-fluid">
    <a class="navbar-brand" href="{{ url_for('contacto') }}">
      <i class="fas fa-envelope"></i> Flask app
    </a>
  </div>
</nav>

<div class="container mt-5">
  <h1 class="mb-4">Contacto</h1>

  <!-- Mostrar errores en una alerta de Bootstrap -->
  {% if errores %}
  <div class="alert alert-danger" role="alert">
    <ul class="mb-0">
      {% for error in errores %}
      <li>{{ error }}</li>
      {% endfor %}
    </ul>
  </div>
  {% endif %}

  <form action="{{ url_for('contacto') }}" method="POST" class="bg-white p-4 rounded shadow-sm">
    <!-- CSRF Token oculto -->
    <input type="hidden" name="csrf_token" value="{{ csrf_token }}">

    <!-- Campo: Nombre -->
    <div class="mb-3">
      <label for="nombre" class="form-label">Nombre</label>
      <div class="input-group">
        <span class="input-group-text"><i class="fas fa-user"></i></span>
        <input
          type="text"
          id="nombre"
          name="nombre"
          class="form-control"
          value="{{ nombre|default('') }}"
          required
        >
      </div>
    </div>

    <!-- Campo: Email -->
    <div class="mb-3">
      <label for="email" class="form-label">Correo electrónico</label>
      <div class="input-group">
        <span class="input-group-text"><i class="fas fa-envelope"></i></span>
        <input
          type="email"
          id="email"
          name="email"
          class="form-control"
          value="{{ email|default('') }}"
          required
        >
      </div>
    </div>

    <!-- Campo: mensaje -->
    <div class="mb-3">
      <label for="mensaje" class="form-label">Mensaje</label>
      <textarea
        id="mensaje"
        name="mensaje"
        class="form-control"
        rows="5"
        required
      >{{ mensaje|default('') }}</textarea>
    </div>

    <!-- Botón de envío -->
    <button type="submit" class="btn btn-primary">
      <i class="fas fa-paper-plane"></i> Enviar
    </button>
  </form>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

El formulario tendrá una apariencia similar a la siguiente:

Si ocurriera un error de validación de datos veríamos un mensaje de color rojo en el formulario. Al añadir mensajes de error y mantener los datos introducidos, proporcionamos una mejor experiencia de usuario, guiándolo a corregir cualquier problema sin tener que reiniciar el proceso. Los mensajes se muestran utilizando las clases de alerta de Bootstrap, como alert alert-danger para errores y alert alert-success para mensajes de éxito.

Página de gracias

Dentro de la carpeta templates, creamos otro archivo para personalizar el mensaje que aparecerá en pantalla cuando el usuario envíe el formulario, le llamaremos gracias.html:

<!-- templates/gracias.html -->
<!DOCTYPE html>
<html lang="es">
<head>
  <meta charset="UTF-8">
  <title>Gracias</title>
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
  <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.0/css/all.min.css" rel="stylesheet">
</head>
<body class="bg-light">

<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
  <div class="container-fluid">
    <a class="navbar-brand" href="{{ url_for('contacto') }}">
      <i class="fas fa-envelope"></i> Flask app
    </a>
  </div>
</nav>

<div class="container mt-5 text-center">
  <h1 class="mb-4">¡Gracias por contactarnos, {{ nombre }}!</h1>
  <p class="fs-5">
    Hemos recibido tu mensaje y te responderemos a la brevedad.
  </p>
  <a href="{{ url_for('contacto') }}" class="btn btn-secondary mt-3">
    <i class="fas fa-arrow-left"></i> Volver a contacto
  </a>
</div>

<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

Esta pantalla se mostrará automáticamente cuando el usuario envíe el formulario y tendrá un aspecto similar a este:

Para probarlo todo en marcha ejecutamos el comando flask run o python app.py en el directorio de flask. Después abrimos el navegador y entramos en localhost:5000/contacto donde ya podremos ver el formulario.

Aplicación Flask para generar contenido con IA

Ahora que ya hemos visto cómo integrar Jinja en nuestros proyectos de Flask, vamos a explorar cómo utilizarlo para mostrar resultados almacenados en base de datos.

Para ello, vamos a desarrollar una aplicación con un formulario que permita pedir a la IA generar publicaciones de LinkedIn que se guarden en base de datos y se muestren por pantalla en HTML con Jinja.

Creación de configuración

Creamos una carpeta flask por ejemplo, en la que crearemos un archivo config.py que tendrá la configuración para conectarse a la base de datos MySQL:

from flask import Flask
from flask_sqlalchemy import SQLAlchemy

usuario = 'dev_user'
contraseña = "tu_password"
servidor = 'mysql-openai-testing.mysql.database.azure.com'
base_datos = 'python_mysql'
ruta_certificado = 'DigiCertGlobalRootG2.crt.pem'

cadena_conexion = (
    f"mysql+pymysql://{usuario}:{contraseña}@{servidor}/{base_datos}"
    f"?ssl_ca={ruta_certificado}&ssl_verify_cert=true"
)

def create_app():
    app = Flask(__name__)
    app.config['SQLALCHEMY_DATABASE_URI'] = cadena_conexion
    app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
    return app

db = SQLAlchemy()

En este archivo configuramos la conexión a base de datos e inicializamos SQLAlchemy. 

Por simplicidad hemos mantenido las credenciales en el archivo, pero por seguridad se puede extraer a un archivo .env y cargarlas con la biblioteca dotenv para python.

Además, en este archivo podemos agregar otras configuraciones que necesitemos.

Creación de esquemas Pydantic

Para interactuar con la API de Open AI con salidas estructuradas, usaremos esquemas de Pydantic. 

Para ello, creamos el archivo schemas.py con el siguiente código:

from pydantic import BaseModel, ConfigDict
from typing import List


class LinkedInPostSchema(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    title: str
    content: str
    hashtags: str
    link: str
    image_prompt: str
    
class LinkedInPostSchemas(BaseModel):
    model_config = ConfigDict(from_attributes=True)
    linkedin_posts: List[LinkedInPostSchema]

En este archivo hemos creado dos esquemas, LinkedInPostSchema representa una publicación de linkedin que será generada por un modelo de IA. Por otro lado, LinkedInPostSchemas es un esquema para envolver una lista de objetos LinkedInPostSchema así podemos trabajar con más de una publicación al mismo tiempo.

Se puede agregar nuevos campos con información que queremos que genere la IA, en este ejemplo, para mantenerlo simple estamos solicitado un título, el contenido de la publicación, un string con una lista de hashtags, un enlace a un recurso externo y un prompt para generar una imagen por si a futuro queremos también generar una imagen por IA para dicha publicación.

Creación de modelos SQLAlchemy

Ahora necesitamos el modelo de SQLAlchemy que generará la tabla de base de datos, y donde almacenaremos los resultados obtenidos.

Creamos un archivo models.py con el siguiente contenido:

from config import db

class LinkedInPost(db.Model):
    __tablename__ = "linkedin_posts"

    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    title = db.Column(db.String(255), nullable=False)
    content = db.Column(db.Text, nullable=False)
    hashtags = db.Column(db.String(500), nullable=True)
    link = db.Column(db.String(500), nullable=True)
    image_prompt = db.Column(db.String(1000), nullable=True)
    image_url = db.Column(db.String(500), nullable=True)
    created_at = db.Column(db.DateTime, default=db.func.current_timestamp())

    def __repr__(self):
        return f"<LinkedInPost id={self.id} title={self.title}>"

    @classmethod
    def from_pydantic(cls, pydantic_model):
        return cls(
            title=pydantic_model.title,
            content=pydantic_model.content,
            image_prompt=pydantic_model.image_prompt,
            hashtags=pydantic_model.hashtags,
            link=pydantic_model.link,
            image_url=None
        )

Este archivo define todos los modelos de base de datos que vamos a usar, en este caso un modelo LinkedInPost para almacenar las publicaciones en base de datos. Además, con un método from_pydantic para facilitarnos la conversión de objeto esquema de Pydantic a objeto modelo de SQLAlchemy.

Desarrollar la app con Jinja

Ahora que ya tenemos todas las piezas, creamos el archivo app.py donde definiremos las rutas de HTTP y plantillas html para poder proporcionar una UI a la aplicación.

Primero añadiremos las importaciones necesarias e inicializaremos la app junto con el cliente OpenAI:

from flask import jsonify, request
from openai import OpenAI
from schemas import LinkedInPostSchema, LinkedInPostSchemas
from config import create_app, db
from models import LinkedInPost

app = create_app()
db.init_app(app)
client = OpenAI(api_key='tu_api_key')

Una vez tenemos la primera parte ya podemos empezar a crear el método que nos permita mostrar las publicaciones de LinkedIn existentes:

@app.route("/", methods=["GET"])
def ver_publicaciones():
    posts = LinkedInPost.query.order_by(LinkedInPost.created_at.desc()).all()
    return render_template("index.html", posts=posts)

Este método carga un archivo index.html donde saldrán los posts de LinkedIn. Por lo que debemos crear la carpeta templates y dentro el archivo index.html con el siguiente contenido:

<!DOCTYPE html>
<html lang="es">
<head>
  <meta charset="UTF-8">
  <title>Generador de contenido</title>
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="bg-light">

<div class="container py-5">
  <h1 class="mb-4">Generador de contenidos de LinkedIn</h1>


  <!-- Listado de posts generados (orden DESC) -->
  <div class="row">
    {% if posts %}
      {% for post in posts %}
        <div class="col-12 mb-4">
          <div class="card">
            <div class="card-body">
              <h5 class="card-title">{{ post.title }}</h5>
              <p class="card-text">{{ post.content }}</p>
              
              {% if post.image_url %}
                <img src="{{ post.image_url }}" alt="Imagen generada" class="img-fluid mb-3">
              {% endif %}
              
              {% if post.hashtags %}
                <p>
                  <strong>Hashtags:</strong>
                  {% for tag in post.hashtags.split(',') %}
                    <span class="badge bg-secondary">{{ tag }}</span>
                  {% endfor %}
                </p>
              {% endif %}
              
              {% if post.image_prompt %}
              <p>Prompt para generar imagen: {{ post.image_prompt }}</p>
              {% endif %}

                <hr>

                {% if post.link %}
                <p>Enlace relacionado: <a href="{{ post.link }}" target="_blank">{{ post.link }}</a></p>
              {% endif %}
            
              <small class="text-muted">Creado: {{ post.created_at }}</small>
            </div>
          </div>
        </div>
      {% endfor %}
    {% else %}
      <p class="text-muted">Todavía no hay publicaciones generadas. ¡Prueba a generar una!</p>
    {% endif %}
  </div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

Por el momento la aplicación solo muestra los contenidos ya existentes en base de datos, para poder generar nuevo contenido necesitaremos crear el método POST en app.py:

@app.route("/", methods=["POST"])
def crear_publicaciones():
    user_prompt = request.form.get("prompt", "")
    completion = client.beta.chat.completions.parse(
        model="gpt-4o-mini",
        messages=[
            {"role": "system", "content": 
                '''
                Eres un asistente de creación de contenidos para marketing en LinkedIn, cada publicación tendrá la siguiente estructura:
                    título (obligatorio),
                    contenido (obligatorio) (sin hashtags),
                    hashtags (obligatorio),
                    link (opcional) (url a un recurso público que permita profundizar más, como una documentación oficial por ejemplo),
                    image_prompt (opcional) (un prompt para generar una imagen con IA que se debería incluir en la publicación relacionada con el tema).
                '''},
            {"role": "user", "content": user_prompt},
        ],
        response_format=LinkedInPostSchemas,
    )
    response = completion.choices[0].message.parsed
    print(response)
    print(response.linkedin_posts)
    linkedin_posts = [LinkedInPost.from_pydantic(post) for post in response.linkedin_posts]
    db.session.add_all(linkedin_posts)
    db.session.commit()
    return redirect(url_for("ver_publicaciones")) # nombre del método de la ruta GET

Para que el usuario pueda escribir el prompt y enviarlo será necesario agregar un textarea a nuestro index.html, debajo del título principal h1:

  <h1 class="mb-4">Generador de contenidos de LinkedIn</h1>
  
  <!-- Formulario para enviar el prompt -->
  <form method="POST" class="mb-5">
    <div class="mb-3">
      <label for="prompt" class="form-label">Prompt para la IA</label>
      <textarea class="form-control" id="prompt" name="prompt" rows="3" 
                placeholder="Ej. Genera un post sobre las ventajas de la IA en startups..."></textarea>
    </div>
    <button type="submit" class="btn btn-primary">Generar Post</button>
  </form>

  <hr>

  <!-- Listado de posts generados (orden DESC) -->

Con este código ya tenemos creada la aplicación que permite generar contenidos con IA y los guarda en base de datos:

Para poder ejecutar la aplicación y que se genere automáticamente las tablas de base de datos necesitamos agregar al final del archivo app.py el siguiente contenido:

if __name__ == '__main__':
    with app.app_context():
        db.create_all()

    app.run(debug=True)

Finalmente, se puede mejorar la interfaz de usuario agregando botones para realizar acciones sobre cada publicación, como por ejemplo editarla, borrarla o simplemente marcarla como leída o publicada.

Para seguir leyendo hazte Plus

¿Ya eres Plus? Accede a la app

20 % DE DESCUENTO

Plan mensual

19.00 /mes

15.20 € /mes

Precio normal mensual: 19 €
58 % DE DESCUENTO

Plan anual

10.00 /mes

8.00 € /mes

Ahorras 132 € al año
Precio normal anual: 120 €
Aprende Flask GRATIS online

Todas las lecciones de Flask

Accede a todas las lecciones de Flask y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.

Accede GRATIS a Flask y certifícate

En esta lección

Objetivos de aprendizaje de esta lección

  • Crear una aplicación con interfaz de usuario utilizando Jinja 2 en Flask
  • Mostrar resultados de IA por interfaz de usuario
  • Poder interactuar con la web generando contenido con IA