Este tutorial adapta pedagógicamente el Día 88. La sección 📝 Contenido original del curso proviene del único archivo de la carpeta:
dia88-tarea7.md(enunciado y pregunta de reflexión, en inglés; aquí traducido). El curso no ofrece guía ni solución, y este tutorial no entrega el sitio terminado: ofrece el análisis, un esqueleto mínimo verificado, pruebas y retos. Todo lo que no está en ese archivo (estructura de la tabla reconstruida del Día 66, script de datos de ejemplo, esqueleto Flask, consideraciones de seguridad, retos 🟢🟡🟠🔴, proyecto extra y la etapa 7 de Mi Portafolio Profesional) es creación de esta adaptación.
Nota sobre las verificaciones: el 28 de septiembre de 2026, con Python 3.14, Flask 3.1.3, Flask-SQLAlchemy 3.1.1 y SQLAlchemy 2.1.1, se ejecutó el esqueleto de la sección 4 con el cliente de pruebas de Flask y una base de datos de ejemplo con dos cafés ficticios: la página principal responde 200; el filtro
?wifi=1muestra solo los cafés con wifi; eliminar con GET responde 405, con POST redirige (302) y borra el café; unidinexistente responde 404. También se comprobó que la base de datos se crea en la carpetainstance/.
⚠️ Archivo que falta: el enunciado dice que incluye
cafes.db, pero no está en la carpeta del Día 88 (ni en otra carpeta del curso descargado). La sección 3 describe su estructura, tomada de la API del Día 66, y ofrece cómo crear una base de datos de ejemplo.
Tabla de contenidos
- La tarea: un sitio web para la API de cafés
- Analizar el problema antes de programar
- 📖 Los datos: la tabla
cafe - 📖 Un esqueleto mínimo con Flask y SQLAlchemy (para completar tú)
- 📖 Agregar y eliminar con seguridad
- 🚀 Proyecto del día: construcción por versiones
- 🌟 Proyecto extra: mapa de lugares de estudio del barrio
- 🏗️ Mi Portafolio Profesional — Etapa 7
- 🧩 Comprueba lo aprendido
- 🤖 Programar con apoyo de IA
- 🛠️ Observaciones sobre el material original
- 📋 Matriz de trazabilidad
- 📦 Evidencias de aprendizaje del Día 88
1. La tarea: un sitio web para la API de cafés
(Fuente: dia88-tarea7.md)
📝 Contenido original del curso
En el Día 66 creamos una API que sirve datos de cafés con wifi y buen café. Hoy vas a usar los datos de ese proyecto para construir un sitio web completo que muestre la información.
La tarea incluye una base de datos SQLite llamada cafes.db con todos los datos de los cafés.
Usando esa base de datos y lo que aprendiste sobre APIs REST y desarrollo web, crea un sitio web que use estos datos. Debe mostrar los cafés, pero también podría permitir agregar nuevos cafés o eliminarlos.
Por ejemplo, una startup de Londres tiene un sitio que hace exactamente esto: laptopfriendly.co/london.
Pregunta de la tarea — Momento de reflexión: cómo abordaste el proyecto; qué fue difícil y qué fácil; cómo mejorarías; tu mayor aprendizaje; qué harías distinto.
Recurso descargable: cafes.db.
Prerequisitos: Flask, plantillas Jinja y herencia de plantillas (Días 54–57); Bootstrap (Día 58); formularios y WTForms (Días 60–62, en especial el proyecto Coffee & Wifi del Día 62); SQLAlchemy (Día 63); la API REST de cafés (Día 66).
2. Analizar el problema antes de programar
(Sección creada para esta adaptación.)
🔍 Antes de programar
| Pregunta | Respuesta que debes construir tú |
|---|---|
| ¿Quién usará el sitio? | Estudiantes o trabajadores que buscan dónde sentarse con el portátil |
| ¿Qué quieren saber rápido? | Dónde queda, si hay wifi, enchufes y baño, cuántos puestos, cuánto vale el café |
| ¿Qué datos tenemos? | La tabla cafe (sección 3) |
| ¿Qué acciones hay? | Ver (todos, uno, filtrar), agregar, eliminar; quizá editar el precio |
| ¿Qué riesgos hay? | Que cualquiera borre cafés; datos inválidos; URL de imágenes rotas |
Dos arquitecturas posibles
| Opción | Cómo funciona | Ventajas | Desventajas |
|---|---|---|---|
| A. Flask lee la base de datos directamente | Las rutas consultan SQLAlchemy y renderizan plantillas | Simple; un solo programa | No se reutiliza la API |
| B. El sitio consume la API del Día 66 | El sitio hace peticiones HTTP a /all, /add… (con requests o JavaScript fetch) |
Separa frontend y backend, como en empresas reales | Dos programas que ejecutar; más complejo |
El enunciado menciona "lo que aprendiste sobre APIs REST": ambas son válidas. Para empezar, la A es más directa; la B es un excelente reto (sección 6).
3. 📖 Los datos: la tabla cafe
(Sección creada para esta adaptación. Estructura reconstruida a partir del Día 66, porque cafes.db no está en la carpeta.)
| Columna | Tipo | Ejemplo | Observación |
|---|---|---|---|
id |
entero, clave primaria | 1 |
La asigna la base de datos |
name |
texto, único | "Café La Biblioteca" |
No puede repetirse |
map_url |
texto | Enlace a un mapa | |
img_url |
texto | Enlace a una imagen | Puede romperse con el tiempo |
location |
texto | "Centro" |
Barrio o zona |
seats |
texto | "20-30" |
Un rango escrito como texto, no un número |
has_toilet, has_wifi, has_sockets, can_take_calls |
booleanos | True |
Baño, wifi, enchufes, se puede hablar por teléfono |
coffee_price |
texto, puede ser nulo | "£2.40" |
Texto con símbolo de moneda: difícil de ordenar |
Explora el archivo (si lo tienes) sin programar: abre cafes.db con un visor de SQLite, o en Python:
import sqlite3
conexion = sqlite3.connect("cafes.db")
print(conexion.execute("SELECT name FROM sqlite_master").fetchall())
print(conexion.execute("SELECT * FROM cafe LIMIT 3").fetchall())Si no tienes cafes.db: crea la tabla con db.create_all() (sección 4) y agrega 5 a 10 cafés ficticios o de tu ciudad (con datos públicos, sin fotos de personas). Así también practicas el formulario de "agregar".
🔍 Analiza el diseño de los datos:
seatsycoffee_priceson texto. ¿Qué problema trae eso si quieres ordenar por precio o filtrar "más de 20 puestos"? ¿Cómo lo mejorarías si diseñaras la tabla desde cero?
4. 📖 Un esqueleto mínimo con Flask y SQLAlchemy (para completar tú)
(Sección creada para esta adaptación. Verificado con el cliente de pruebas de Flask.)
cafes_web/
├── app.py
├── instance/
│ └── cafes.db <- aquí la busca Flask-SQLAlchemy
└── templates/
└── index.htmlapp.py (fragmento; el modelo tiene todas las columnas de la sección 3, como en el Día 66):
from flask import Flask, render_template, request, redirect, url_for
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from sqlalchemy import String, Boolean, Integer
class Base(DeclarativeBase):
pass
app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///cafes.db"
db = SQLAlchemy(model_class=Base)
db.init_app(app)
class Cafe(db.Model):
id: Mapped[int] = mapped_column(Integer, primary_key=True)
name: Mapped[str] = mapped_column(String(250), unique=True, nullable=False)
location: Mapped[str] = mapped_column(String(250), nullable=False)
has_wifi: Mapped[bool] = mapped_column(Boolean, nullable=False)
# ... el resto de columnas de la sección 3
@app.route("/")
def inicio():
consulta = db.select(Cafe).order_by(Cafe.name)
if request.args.get("wifi") == "1":
consulta = consulta.where(Cafe.has_wifi == True)
cafes = db.session.execute(consulta).scalars().all()
return render_template("index.html", cafes=cafes)Qué hace cada parte:
sqlite:///cafes.dbcon Flask-SQLAlchemy 3 busca el archivo en la carpetainstance/junto aapp.py(verificado). Si ponescafes.dbal lado deapp.py, se creará una base vacía eninstance/y no verás ningún café.db.select(Cafe).order_by(Cafe.name)construye una consulta; todavía no la ejecuta.request.args.get("wifi")lee el parámetro de la URL:/?wifi=1. Así un mismoiniciosirve para "todos" y "solo con wifi"..where(Cafe.has_wifi == True)agrega una condición. (Los analizadores de estilo sugieren.is_(True); ambas funcionan en SQLAlchemy.).scalars().all()devuelve una lista de objetosCafe.
templates/index.html (fragmento):
{% for cafe in cafes %}
<article>
<h2>{{ cafe.name }}</h2>
<p>{{ cafe.location }}</p>
{% if cafe.has_wifi %}<span>📶 Wifi</span>{% endif %}
</article>
{% else %}
<p>No hay cafés que cumplan esa condición.</p>
{% endfor %}El {% else %} de un for en Jinja se muestra si la lista está vacía.
Tu turno (sin solución):
- Página de detalle
/cafe/<int:cafe_id>que usedb.get_or_404(Cafe, cafe_id). - Tarjetas con Bootstrap: imagen, íconos de servicios, enlace "Ver en el mapa".
- Filtros combinables: wifi, enchufes, baño, barrio.
⚠️ Errores frecuentes
| Error | Qué ocurre | Solución |
|---|---|---|
| La página no muestra ningún café | Se creó una base de datos vacía nueva | Coloca cafes.db en instance/ o usa una ruta absoluta |
no such table: cafe |
La tabla no existe en esa base | Revisa la ruta; si es nueva, db.create_all() dentro de app.app_context() |
RuntimeError: Working outside of application context |
Consultas fuera de una ruta | with app.app_context(): |
| La imagen no aparece | URL externa rota | Usa onerror en <img> para mostrar una imagen por defecto, o valida la URL al agregar |
IntegrityError: UNIQUE constraint failed |
Se agregó un café con nombre repetido | Captura la excepción y muestra un mensaje amable |
5. 📖 Agregar y eliminar con seguridad
(Sección creada para esta adaptación.)
Eliminar: nunca con un enlace GET
Un enlace <a href="/eliminar/3"> parece cómodo, pero los navegadores, buscadores y extensiones pueden "visitar" enlaces solos, y un enlace se puede enviar por chat para engañar a alguien. Las acciones que cambian datos van por POST (o DELETE en una API):
@app.route("/cafe/<int:cafe_id>/eliminar", methods=["POST"])
def eliminar(cafe_id):
cafe = db.get_or_404(Cafe, cafe_id)
db.session.delete(cafe)
db.session.commit()
return redirect(url_for("inicio"))<form method="post" action="{{ url_for('eliminar', cafe_id=cafe.id) }}">
<button onclick="return confirm('¿Eliminar este café?')">Eliminar</button>
</form>Verificado: un GET a esa ruta responde 405 Method Not Allowed; un POST borra y redirige.
¿Quién puede eliminar?
En la API del Día 66, borrar exigía una clave (api_key). En un sitio web, lo adecuado es que solo un administrador autenticado borre (Flask-Login, Día 68). Si aún no quieres autenticación, una alternativa es que cualquiera proponga un café y el administrador lo apruebe (una columna aprobado).
Agregar: validar
Con Flask-WTForms (Días 61–62) valida: nombre obligatorio, URLs válidas (URL()), precio con formato. Y además protege contra CSRF (el csrf_token que WTForms agrega a los formularios).
6. 🚀 Proyecto del día: construcción por versiones
(Desarrolla la tarea dia88-tarea7.md. Las versiones son una propuesta de esta adaptación.)
Problema
Construir un sitio web que muestre la base de datos de cafés y permita agregar y eliminar cafés.
Contexto
Muchos estudiantes buscan dónde hacer tareas con buena conexión y enchufes. Tu sitio los ayuda a elegir.
Objetivo
Un sitio Flask con listado, detalle, filtros, formulario para agregar y eliminación protegida.
Requisitos funcionales
- Listado de cafés en tarjetas con sus servicios.
- Página de detalle de cada café.
- Filtros (al menos wifi y enchufes).
- Formulario validado para agregar cafés.
- Eliminación por POST, con confirmación.
Restricciones técnicas
- Flask + SQLAlchemy (o consumo de la API del Día 66, si eliges la arquitectura B).
- Plantilla base con herencia (
{% extends %}). - Ninguna acción que modifique datos por GET.
Planificación
- Comprender el problema (visita el sitio de ejemplo del enunciado). 2. Entradas: la base de datos y los formularios. 3. Procesos: consultar, filtrar, insertar, borrar. 4. Salidas: páginas HTML. 5. Dividir por rutas. 6. Bocetar las páginas. 7. Implementar. 8. Probar. 9. Corregir. 10. Mejorar.
Construcción paso a paso
- Versión 1 — Datos: explorar
cafes.db(o crear los cafés de ejemplo) y mostrar los nombres en una lista simple. - Versión 2 — Tarjetas: plantilla base, Bootstrap, íconos de servicios.
- Versión 3 — Detalle y filtros.
- Versión 4 — Agregar: formulario WTForms validado y manejo de nombres repetidos.
- Versión 5 — Eliminar: POST, confirmación y (idealmente) solo administradores.
- Versión final — Pulido: página 404 personalizada,
README.md, diario.
Pruebas
cliente = app.test_client()
assert cliente.get("/").status_code == 200
assert cliente.get("/cafe/9999").status_code == 404
assert cliente.get("/cafe/1/eliminar").status_code == 405 # GET no permitido| # | Caso | Resultado esperado |
|---|---|---|
| 1 | /?wifi=1 |
Solo cafés con wifi |
| 2 | Agregar un café con nombre repetido | Mensaje de error, sin excepción |
| 3 | Agregar con URL inválida | El formulario lo rechaza |
| 4 | Eliminar sin ser administrador (si hay login) | 403 o redirección al login |
| 5 | Café con imagen rota | Se muestra una imagen por defecto |
| 6 | Base de datos vacía | Mensaje "No hay cafés" |
🧪 Prueba y depuración
@app.route("/eliminar/<cafe_id>")
def eliminar(cafe_id):
cafe = Cafe.query.get(cafe_id)
db.session.delete(cafe)
return redirect("/")Encuentra cuatro problemas: se elimina por GET; si el id no existe, cafe es None y falla (usar get_or_404); falta db.session.commit() (el cambio no se guarda); y Cafe.query.get es la API antigua (SQLAlchemy 2 recomienda db.session.get o db.get_or_404). Además, <cafe_id> sin int: recibe texto.
⭐ Extensión del proyecto
- 🟢 Reto 1 — Aplicación: búsqueda por nombre o barrio (
Cafe.name.ilike(f"%{texto}%")). - 🟡 Reto 2 — Ampliación: editar el precio del café (como el PATCH del Día 66, pero con un formulario).
- 🟠 Reto 3 — Desafío: arquitectura B: el sitio consume la API del Día 66 con
requests. Maneja el caso en que la API no responde. - 🔴 Reto 4 — Pensamiento computacional: calificaciones de usuarios (1–5 estrellas). Diseña la nueva tabla, su relación con
cafe, cómo evitar que una persona califique 100 veces y cómo ordenar por calificación sin que un café con una sola reseña de 5 supere a uno con 50 reseñas de 4,8.
7. 🌟 Proyecto extra: mapa de lugares de estudio del barrio
(Proyecto creado para esta adaptación; no existe en el curso original.)
Situación
El personero estudiantil quiere un directorio de lugares públicos para estudiar cerca del colegio: bibliotecas, casas de la cultura, parques con wifi gratuito, puntos digitales.
Requisitos
- Tu propia tabla (diseñada por ti) con horario, costo (gratis o no), wifi, accesibilidad para sillas de ruedas y tipo de lugar.
- Listado con filtros y página de detalle.
- Formulario para proponer lugares que el administrador aprueba antes de publicar.
Decisiones que debes tomar (y justificar)
- ¿Cómo guardas el horario para poder filtrar "abierto ahora"?
- ¿Qué datos no publicas para proteger la privacidad (por ejemplo, el nombre de quien propone)?
Casos de prueba
- Un lugar propuesto no aparece hasta ser aprobado.
- El filtro "gratis y con wifi" combina ambas condiciones.
8. 🏗️ Mi Portafolio Profesional — Etapa 7
(Proyecto integrador creado para esta adaptación; etapas anteriores en los Días 82 a 87.)
Etapa 7 (hoy):
- Repositorio del sitio de cafés con
requirements.txt,.gitignore(excluyeinstance/,.envyvenv/) y un scriptdatos_ejemplo.pyque llene la base con cafés ficticios, para que cualquiera pueda probarlo. README.mdcon capturas, las rutas del sitio y cómo ejecutarlo.DIARIO.mddel Día 88 y ficha enproyectos.json.- Si publicas el sitio (Día 71), desactiva
debugy protege la eliminación.
9. 🧩 Comprueba lo aprendido
Conceptuales
- ¿Por qué una acción que borra datos no debe hacerse con GET?
- ¿Qué diferencia hay entre las arquitecturas A y B de la sección 2?
- ¿Qué problema tiene guardar el precio como texto (
"£2.40")? - ¿Qué hace
db.get_or_404?
Análisis de código
consulta = db.select(Cafe).where(Cafe.has_sockets == True).order_by(Cafe.location)¿Qué cafés devuelve y en qué orden?
Predicción
Tu app.py usa sqlite:///cafes.db y copiaste cafes.db junto a app.py. ¿Qué verás al abrir el sitio?
Depuración
"Elimino un café, me redirige al inicio, pero el café sigue ahí." ¿Qué línea falta?
Modificación
Agrega a inicio un filtro ?barrio=Centro que se combine con el de wifi.
Creación
Escribe la ruta /aleatorio que redirija a la página de detalle de un café al azar (idea del Día 66).
10. 🤖 Programar con apoyo de IA
Ciclo de trabajo: formula el problema → inténtalo tú primero → consulta a la IA sobre un punto concreto → lee y entiende → prueba → compara con la documentación oficial → modifica → explica con tus palabras.
Actividad 1 — API antigua o nueva. Pide a una IA código para consultar todos los registros con Flask-SQLAlchemy. Si usa Cafe.query.all(), busca en la documentación de Flask-SQLAlchemy 3 cómo se hace con db.session.execute(db.select(...)) y anota por qué la documentación prefiere la nueva forma.
Actividad 2 — Auditoría de seguridad. Pega tus rutas (sin claves) y pregunta qué riesgos ves. Comprueba cada afirmación con pruebas del cliente de Flask.
Actividad 3 — Datos de ejemplo. Pide a una IA 10 cafés ficticios en formato de diccionario. Revisa que no sean negocios reales con datos inventados (sería desinformación) y márcalos claramente como ficticios.
Declara el uso: sección "Uso de IA" en el README.md.
11. 🛠️ Observaciones sobre el material original
| Archivo | Observación | Tratamiento |
|---|---|---|
dia88-tarea7.md |
Anuncia cafes.db como recurso, pero no está en la carpeta |
Sección 3: estructura reconstruida del Día 66 y cómo crear datos de ejemplo |
dia88-tarea7.md |
"Permitir eliminar cafés" sin mencionar quién puede hacerlo | Sección 5: POST, confirmación y autenticación |
dia88-tarea7.md |
Sitio de ejemplo de una startup de Londres | Se conserva como inspiración; puede haber cambiado o dejado de existir |
| Día 66 (referencia) | Guarda precio y puestos como texto | Se analiza como decisión de diseño mejorable (sección 3) |
12. 📋 Matriz de trazabilidad
| Contenido original | Archivo de origen | Concepto desarrollado | Ejercicio original | Reto adicional | Proyecto relacionado |
|---|---|---|---|---|---|
| Usar los datos de la API del Día 66 para un sitio completo | dia88-tarea7.md |
Secciones 2 y 3 | Sitio de cafés | Reto 3 (consumir la API) | Secciones 6 y 8 |
Base de datos SQLite cafes.db |
dia88-tarea7.md |
Secciones 3 y 4 | Sitio de cafés | — | Sección 6 |
| Mostrar cafés; agregar o eliminar | dia88-tarea7.md |
Secciones 4 y 5 | Sitio de cafés | Retos 1, 2 y 4 | Sección 6 |
| Ejemplo: laptopfriendly.co | dia88-tarea7.md |
Sección 2 | — | — | — |
| Momento de reflexión | dia88-tarea7.md |
Sección 6 (versión final) | Pregunta de reflexión | — | Sección 8 |
| (No existe en el curso original) | — | Seguridad (POST, CSRF, autenticación) | — | Lugares de estudio del barrio | Sección 7 |
13. 📦 Evidencias de aprendizaje del Día 88
- El sitio funcionando: capturas del listado, un filtro, el detalle y el formulario.
- Las pruebas del cliente de Flask superadas.
datos_ejemplo.py,README.md,DIARIO.mdy ficha en el portafolio.- Cuaderno de errores: base vacía por la carpeta
instance/,no such table, contexto de aplicación, eliminar por GET, falta decommit, nombre repetido, API antiguaModel.query.