Saltar a contenido

Lab guiado: Integración — mensajes flash y API REST mínima


Punto de partida

Parte del zip que entregaste al final del lab a5. Antes de empezar:

  1. Activa el entorno virtual del proyecto.
  2. Sitúate en la carpeta padre de expendedora/.
  3. Si la BD expendedora.db no existe (porque partes del zip y no traes el fichero), créala con python expendedora/crear_bd.py desde la raíz del proyecto. Si ya la tenías de a5, sáltate este paso.
  4. Arranca el servidor: python -m expendedora.presentation.app.
  5. Comprueba que los formularios de a5 siguen funcionando: insertar saldo, seleccionar un producto que exista en tu BD, confirmar la compra (verás mensaje.html con el cambio — ahí es donde quedó a medio el patrón PRG), agregar un producto nuevo y eliminar un producto existente.

Sobre este lab

Es la fase de integración funcional de la app web. Tiene dos bloques: mensajes flash para dar feedback al usuario tras un redirect (cierra el patrón PRG que dejaste a medio en a5) y una API REST mínima en JSON que comparte el servicio con la interfaz web (demuestra que la capa de presentación es realmente intercambiable). El cierre arquitectónico de la unidad —recorrer el proyecto y comprobar qué se ha tocado y qué no— vendrá en la Fase 8 (auditoría).


Antes de empezar: repasemos los conceptos

Mensajes flash Flask permite "guardar un mensaje" en la sesión del usuario para mostrarlo en la siguiente petición. Útil tras un POST con redirect: el mensaje sobrevive al redirect y aparece una vez en la página de destino. Resuelve el problema de "redirigí a /saldo y ya no muestro el cambio que devolvió /comprar".

API REST En vez de devolver HTML, una ruta puede devolver datos en formato JSON. Útil para que otros programas (apps móviles, scripts) consuman la misma lógica de negocio sin parsear HTML. Flask convierte automáticamente diccionarios y listas a JSON con jsonify.


Paso 1 — Mensajes flash para feedback tras redirect app.py, base.html

Conceptos

Un mensaje flash necesita una clave secreta en la app (app.secret_key) porque Flask los guarda firmados en una cookie. Es la primera vez que la app necesita "estado entre peticiones" más allá de la base de datos.

Objetivo

Mostrar mensajes informativos tras acciones que terminan en redirect.

Tarea

En app.py, añade una clave secreta tras crear app:

app = Flask(__name__)
app.secret_key = 'cambiar-esto-en-produccion'  # solo para desarrollo

La clave secreta es secreta

En producción esta clave no se escribe literalmente en el código: viene de una variable de entorno o de un fichero .env que va al .gitignore. Para este lab vale el string literal — el alumno lo aprenderá en el módulo de despliegue.

Importa flash junto a las demás importaciones de Flask:

from flask import Flask, redirect, url_for, request, render_template, flash

Modifica el route /comprar para que, tras éxito, redirija a / con un mensaje flash en lugar de renderizar mensaje.html:

@app.route('/comprar', methods=['GET', 'POST'])
def comprar():
    if request.method == 'POST':
        try:
            cambio = servicio.comprar()
            importe = f"{cambio:.2f}".replace(".", ",")
            flash(f"Compra realizada. Cambio: {importe} EUR.", 'exito')
            return redirect(url_for('inicio'))
        except ValueError as e:
            return render_template('comprar.html', saldo=servicio.saldo(), error=str(e)), 400
    return render_template('comprar.html', saldo=servicio.saldo(), error=None)

Haz lo mismo con /cancelar:

1
2
3
4
5
6
@app.route('/cancelar', methods=['POST'])
def cancelar():
    devuelto = servicio.cancelar()
    importe = f"{devuelto:.2f}".replace(".", ",")
    flash(f"Operación cancelada. Devuelto: {importe} EUR.", 'info')
    return redirect(url_for('inicio'))

Y el route /eliminar/<codigo> tras éxito. Solo se cambia la línea resaltada dentro del bloque if request.method == 'POST':: la rama GET del route (la que renderiza la página de confirmación con eliminar.html) se queda intacta.

1
2
3
4
5
6
7
if request.method == 'POST':
    try:
        servicio.eliminar_producto(codigo)
        flash(f"Producto {codigo} eliminado.", 'exito')
        return redirect(url_for('listar_productos'))
    except ProductoNoEncontradoError as e:
        return f"Error: {e}", 404

Modifica presentation/templates/base.html para mostrar los mensajes flash en todas las páginas. Añade el bloque dentro de <main>:

<main>
    {% with mensajes = get_flashed_messages(with_categories=true) %}
        {% if mensajes %}
            <ul class="flashes">
            {% for categoria, msg in mensajes %}
                <li class="flash flash-{{ categoria }}">{{ msg }}</li>
            {% endfor %}
            </ul>
        {% endif %}
    {% endwith %}

    {% block contenido %}{% endblock %}
</main>

{% with %} en Jinja2

{% with nombre = expresión %}…{% endwith %} crea una variable local que solo existe dentro del bloque. Aquí guardamos en mensajes el resultado de get_flashed_messages(with_categories=true) para no llamar a la función dos veces (una en el {% if %} y otra en el {% for %}). Es la primera vez que aparece esta etiqueta en UT4: las que usaste hasta a5 eran {% for %}, {% if %}, {% block %} y {% extends %}.

Puedes borrar también mensaje.html ya que comprar y cancelar no lo usan (eliminar tampoco lo usaba).

Pregunta

Antes de este paso, tras /comprar veías el cambio en una página dedicada. Ahora vas a / y el mensaje aparece en la cabecera. ¿Qué ganas y qué pierdes con el cambio?

Respuesta

Ganas: una sola página de inicio que centraliza la información, mensajes consistentes en estilo, y la URL final del flujo (/) es siempre la misma — recargar es seguro. Pierdes: el cambio era el dato más importante de la operación de compra y ahora compite visualmente con el resto de la página de inicio. En aplicaciones reales esto se compensa con estilos llamativos para los flash o pantallas dedicadas para operaciones críticas; aquí lo dejamos simple.

Comprobación

  • Flujo completo: /seleccionar → A1, /insertar → 2.0, /comprar → confirmar. Aterrizas en / con un mensaje flash arriba.
  • Recarga /: el mensaje desaparece (los flash son de un solo uso).
  • /cancelar (con saldo) → flash con el saldo devuelto en /.
  • /eliminar/A1 → confirma → flash en /productos.

Reflexión

Respuestas
  • Los flash viven en la cookie del navegador (firmada con la clave secreta). Solo este navegador los ve.
  • Son mensajes "de un uso": los lee get_flashed_messages y se vacían.
  • La separación se mantiene: el route guarda mensaje + redirige; el template los muestra.

Paso 2 — API REST mínima en JSON app.py

Conceptos

Una API REST expone los datos del dominio en formato JSON, sin HTML. Misma capa de aplicación, distinta capa de presentación: en lugar de render_template, devolvemos jsonify. Esto demuestra hasta qué punto es real la separación de capas que llevamos toda la unidad defendiendo.

Objetivo

Añadir un par de endpoints /api/productos y /api/producto/<codigo> que devuelvan JSON.

Tarea

Importa jsonify junto a las demás funciones de Flask:

from flask import Flask, redirect, url_for, request, render_template, flash, jsonify

Añade los routes al final del fichero:

@app.route('/api/productos')
def api_productos():
    keys = ['codigo', 'nombre', 'precio_base', 'precio_final', 'cantidad', 'descuento']
    productos = [dict(zip(keys, t)) for t in servicio.listar_productos()]
    return jsonify(productos)


@app.route('/api/producto/<codigo>')
def api_producto(codigo):
    try:
        keys = ['codigo', 'nombre', 'precio_base', 'precio_final', 'cantidad', 'descuento']
        producto = dict(zip(keys, servicio.obtener_producto(codigo)))
        return jsonify(producto)
    except ProductoNoEncontradoError as e:
        return jsonify({'error': str(e)}), 404

El servicio no cambia

api_productos llama a la misma servicio.listar_productos() que /productos. La capa de aplicación no se entera de si el cliente quiere HTML o JSON; eso lo decide la presentación. Esto es exactamente la promesa de la arquitectura por capas.

Pregunta

¿Qué cambiaría en application/, domain/ o infrastructure/ para añadir esta API REST?

Respuesta

Nada. Ni una línea. La API REST es un nuevo módulo en la capa de presentación — exactamente como Flask fue un nuevo módulo en la capa de presentación cuando llegamos en a1. La arquitectura permite añadir interfaces sin tocar el resto. En la Fase 8 lo comprobarás fichero a fichero en la auditoría.

Comprobación

Desde otra terminal:

curl http://localhost:5000/api/productos
curl http://localhost:5000/api/producto/A1
curl -i http://localhost:5000/api/producto/ZZ
  • El primer comando devuelve un array JSON con todos los productos.
  • El segundo, un objeto JSON con el producto A1.
  • El tercero, un JSON {"error": "..."} con código 404.

Reflexión

Respuestas
  • jsonify serializa diccionarios y listas a JSON con cabecera Content-Type: application/json.
  • Los routes API y los routes web son hermanos de la misma capa de presentación. Ninguno sabe del otro.
  • En proyectos grandes los routes API se ponen en un fichero distinto (presentation/api.py) y se registran como blueprint. Aquí los dejamos juntos por simplicidad.

Paso 3 — Actualizar la documentación

Tarea

CHANGELOG.md

## [0.10.0] - (Fase 6: integración — mensajes flash y API REST mínima)

### Added
- `presentation/app.py`: clave secreta para mensajes flash.
- `presentation/app.py`: routes `/api/productos` y `/api/producto/<codigo>` que devuelven JSON.
- `presentation/templates/base.html`: bloque que muestra los mensajes flash de la sesión.

### Changed
- `presentation/app.py`: routes `/comprar`, `/cancelar`, `/eliminar/<codigo>` usan `flash` + `redirect` tras éxito en lugar de `render_template('mensaje.html', ...)`.

### Removed
- `presentation/templates/mensaje.html`: sustituido por mensajes flash en `base.html`.

README.md

Añade al final de la sección Uso (interfaz web):

**API REST (JSON):**
- `GET /api/productos` — lista de productos en JSON.
- `GET /api/producto/<codigo>` — detalle de un producto en JSON (404 + JSON `{"error": ...}` si no existe).

Comprobación

  • curl http://localhost:5000/api/productos | python -m json.tool — JSON formateado.
  • Recorrido visual: arranca el servidor, ejecuta el flujo de compra completo y observa los mensajes flash.

Checklist de entrega

  • app.secret_key declarada (con la advertencia de que no es production-grade).
  • Mensajes flash funcionando tras /comprar, /cancelar y /eliminar/<codigo>.
  • base.html muestra los flash con categoría.
  • mensaje.html eliminado.
  • API REST: /api/productos y /api/producto/<codigo> devuelven JSON correcto.
  • Todos los tests del proyecto siguen pasando.
  • CHANGELOG.md con entrada 0.10.0 y README.md actualizados.

Problemas comunes

RuntimeError: The session is unavailable because no secret key was set

Olvidaste declarar app.secret_key. Los flash necesitan sesión, y la sesión necesita clave para firmarse.

El mensaje flash no aparece tras el redirect

Asegúrate de que base.html lleva el bloque get_flashed_messages antes del {% block contenido %}. Si solo está en una página específica, los flash que aparezcan tras un redirect a otra página no se mostrarán.

La API JSON devuelve null o algo raro

Comprueba que devuelves jsonify(...) y no json.dumps(...). jsonify añade la cabecera Content-Type: application/json correcta; json.dumps solo serializa.