Saltar al contenido
Volver al inicio 100 Días de Código en Python — Día 88: Proyecto de portafolio — Sitio web de cafés con wifi

100 Días de Código en Python — Día 88: Proyecto de portafolio — Sitio web de cafés con wifi

Nivel: Grado décimo Área: Tecnología e Informática Modalidad: Tutorial práctico Fuente: Curso "100 Days of Code: The Complete Python Pro Bootcamp" (Angela Yu, App Brewery). Contenido del Día 88, proyecto de portafolio profesional (categoría Web Development).

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=1 muestra solo los cafés con wifi; eliminar con GET responde 405, con POST redirige (302) y borra el café; un id inexistente responde 404. También se comprobó que la base de datos se crea en la carpeta instance/.

⚠️ 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

  1. La tarea: un sitio web para la API de cafés
  2. Analizar el problema antes de programar
  3. 📖 Los datos: la tabla cafe
  4. 📖 Un esqueleto mínimo con Flask y SQLAlchemy (para completar tú)
  5. 📖 Agregar y eliminar con seguridad
  6. 🚀 Proyecto del día: construcción por versiones
  7. 🌟 Proyecto extra: mapa de lugares de estudio del barrio
  8. 🏗️ Mi Portafolio Profesional — Etapa 7
  9. 🧩 Comprueba lo aprendido
  10. 🤖 Programar con apoyo de IA
  11. 🛠️ Observaciones sobre el material original
  12. 📋 Matriz de trazabilidad
  13. 📦 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:

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: seats y coffee_price son 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.)

Texto
cafes_web/
├── app.py
├── instance/
│   └── cafes.db          <- aquí la busca Flask-SQLAlchemy
└── templates/
    └── index.html

app.py (fragmento; el modelo tiene todas las columnas de la sección 3, como en el Día 66):

Python
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.db con Flask-SQLAlchemy 3 busca el archivo en la carpeta instance/ junto a app.py (verificado). Si pones cafes.db al lado de app.py, se creará una base vacía en instance/ 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 mismo inicio sirve 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 objetos Cafe.

templates/index.html (fragmento):

HTML
{% 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):

  1. Página de detalle /cafe/<int:cafe_id> que use db.get_or_404(Cafe, cafe_id).
  2. Tarjetas con Bootstrap: imagen, íconos de servicios, enlace "Ver en el mapa".
  3. 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):

Python
@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"))
HTML
<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

  1. Listado de cafés en tarjetas con sus servicios.
  2. Página de detalle de cada café.
  3. Filtros (al menos wifi y enchufes).
  4. Formulario validado para agregar cafés.
  5. 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

  1. 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

Python
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

Python
@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

  1. Tu propia tabla (diseñada por ti) con horario, costo (gratis o no), wifi, accesibilidad para sillas de ruedas y tipo de lugar.
  2. Listado con filtros y página de detalle.
  3. 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):

  1. Repositorio del sitio de cafés con requirements.txt, .gitignore (excluye instance/, .env y venv/) y un script datos_ejemplo.py que llene la base con cafés ficticios, para que cualquiera pueda probarlo.
  2. README.md con capturas, las rutas del sitio y cómo ejecutarlo.
  3. DIARIO.md del Día 88 y ficha en proyectos.json.
  4. Si publicas el sitio (Día 71), desactiva debug y protege la eliminación.

9. 🧩 Comprueba lo aprendido

Conceptuales

  1. ¿Por qué una acción que borra datos no debe hacerse con GET?
  2. ¿Qué diferencia hay entre las arquitecturas A y B de la sección 2?
  3. ¿Qué problema tiene guardar el precio como texto ("£2.40")?
  4. ¿Qué hace db.get_or_404?

Análisis de código

Python
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.md y 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 de commit, nombre repetido, API antigua Model.query.