Saltar al contenido
Volver al inicio 100 Días de Código en Python — Día 62: Proyecto "Coffee & Wifi" — Flask, WTForms, Bootstrap y CSV

100 Días de Código en Python — Día 62: Proyecto "Coffee & Wifi" — Flask, WTForms, Bootstrap y CSV

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 62 (nivel avanzado).

Este tutorial adapta pedagógicamente el Día 62, un proyecto de integración de los Días 58 a 61. Las secciones 📝 Ejercicio original del curso provienen de las lecciones 440 y 442; los archivos de inicio (day-62-starting-files-coffee-and-wifi.zip) y la solución (day-62-coffee-and-wifi-end.zip) están en los recursos del curso, no entre los archivos suministrados, así que aquí no se reproduce su contenido exacto. El archivo de la lección 441 ("ver el producto final") no contiene esa lección, sino el texto publicitario del curso (ver la matriz de trazabilidad). Los retos 🟢🟡🟠🔴, las secciones sobre CSV con acentos y filtros de Jinja, el proyecto extra y la etapa 4 del proyecto integrador son creaciones de esta adaptación. Las correcciones al material original aparecen como 🛠️ Nota técnica.

Nota sobre las verificaciones: el 27 de septiembre de 2026, con Python 3.14 (en Windows), Flask 3.1.3, Flask-WTF 1.3.0, WTForms 3.2.2 y Bootstrap-Flask 2.6.0, se comprobó: qué direcciones acepta el validador URL(), que leer un CSV en UTF-8 sin encoding="utf-8" en Windows produce texto dañado (Café → Café) o UnicodeDecodeError con ciertos emojis, y que agregar filas "a mano" a un CSV sin salto de línea final une dos registros en la misma línea.


Tabla de contenidos

  1. Antes de empezar: un proyecto como los de un cliente real
  2. El proyecto de inicio
  3. Los requisitos, uno por uno
  4. 📖 CSV sin sorpresas: codificación, comas y saltos de línea
  5. 🚀 Proyecto del día: Coffee & Wifi
  6. 🌟 Proyecto extra: el mapa de lugares para estudiar en el barrio
  7. 🏗️ Proyecto integrador del bloque avanzado — Etapa 4
  8. 🧩 Comprueba lo aprendido
  9. 🤖 Programar con apoyo de IA
  10. 🛠️ Correcciones técnicas al material original
  11. 📋 Matriz de trazabilidad
  12. 📦 Evidencias de aprendizaje del Día 62

0. Antes de empezar: un proyecto como los de un cliente real

(Fuente: 442-check-off-each-requirement.md)

📝 Contenido original del curso

"Como en un proyecto real con un cliente", el frontend ya está hecho: tu trabajo es el backend. El objetivo del día es sentirte cómodo con Flask-WTF, Bootstrap-Flask, las clases de Bootstrap y repasar el manejo de CSV. (Si quieres más práctica de HTML y Bootstrap, puedes construir todo desde cero.)

El sitio, Coffee & Wifi, reúne cafeterías de Londres con buen café, wifi y enchufes para trabajar con el portátil.

Sobre cómo aprender: "El aprendizaje ocurre cuando te atascas y resuelves tus problemas". La profesora recomienda dedicar al menos una hora antes de mirar la solución; si llevas media hora atascado en el paso 1, "toma un descanso, sal a caminar, come algo y vuelve al código". Y recuerda: "hay varias formas de hacer lo mismo; la solución de ejemplo no es la única".

Prerequisitos: Bootstrap (Día 58), herencia de plantillas, Flask-WTF, validadores y Bootstrap-Flask (Día 61), CSV (Día 25), bucles en Jinja (Día 57).


1. El proyecto de inicio

(Fuente: 440-download-the-starting-project.md)

📝 Ejercicio original del curso

  1. Descarga y descomprime los archivos de inicio.
  2. Ábrelos en PyCharm y acepta crear el entorno virtual e instalar requirements.txt. Si hay subrayados rojos: File → Reload All from Disk.
  3. Si no te lo ofrece, crea el entorno a mano (Add Local Interpreter, sin "inherit global site-packages").
  4. Si aún faltan paquetes: python -m pip install -r requirements.txt (Windows) o pip3 install -r requirements.txt (macOS).
  5. Explora el proyecto: hay un archivo .csv con datos de algunas cafeterías de Londres.

🔍 Antes de programar

Antes de escribir una línea, lee el proyecto como si fuera de otra persona:

  • ¿Qué rutas existen ya en main.py? ¿Cuáles faltan?
  • ¿Qué plantillas hay y cuál hereda de cuál?
  • Abre el CSV con un editor de texto (no solo con Excel): ¿cuáles son las columnas?, ¿qué separador usa?, ¿hay emojis o símbolos?, ¿termina con un salto de línea?
  • ¿Qué variables espera cada plantilla (cafes, form…)?

2. Los requisitos, uno por uno

(Fuente: 442-check-off-each-requirement.md)

📝 Ejercicio original del curso: los requisitos

  1. Portada con estilo: la página de inicio debe usar css/styles.css. Pista: agrega a base.html los bloques de la plantilla de inicio de Bootstrap-Flask y enlaza la hoja de estilos.
  2. Ruta /cafes: debe renderizar cafes.html con una tabla de Bootstrap con todos los datos de cafe-data.csv. Pista: la ruta envía un objeto cafes; ponlo dentro de un <p> para ver cómo es.
  3. Enlace al mapa: la URL de la ubicación debe mostrarse como un <a> con el texto "Maps Link", no como el enlace completo. Pista: todos los enlaces de ubicación empiezan por "http".
  4. El botón "Show Me!" de la portada lleva a cafes.html.
  5. Ruta secreta /add: no tiene botón, pero "quien la conozca" puede entrar; renderiza add.html.
  6. En add.html, un formulario con WTForms y render_form con todos los campos de la demostración.
  7. El campo de la ubicación debe validar que sea una URL. (El curso enlaza cómo desactivar la validación del navegador.)
  8. Al enviar el formulario con éxito, los datos se agregan al final de cafe-data.csv, separados por comas como las demás líneas.
  9. Todos los enlaces de navegación deben funcionar.

📖 Guía de razonamiento por requisito (adaptación, sin la solución completa)

Requisito 2 — leer y mostrar el CSV. El patrón para leer un CSV como lista de filas:

Python
import csv

with open("cafe-data.csv", newline="", encoding="utf-8") as archivo:
    lector = csv.reader(archivo, delimiter=",")
    filas = list(lector)          # la fila 0 son los encabezados

En la plantilla, dos ciclos anidados: uno por fila y, dentro, uno por celda. Para separar los encabezados puedes usar filas[0] y filas[1:], o loop.first de Jinja.

Requisito 3 — el enlace. Dentro del ciclo de celdas:

HTML
{% if celda[0:4] == "http" %}
    <td><a href="{{ celda }}">Maps Link</a></td>
{% else %}
    <td>{{ celda }}</td>
{% endif %}

Jinja permite rebanadas ([0:4]) igual que Python. También sirve celda.startswith("http").

Requisito 6 — el formulario. Una clase CafeForm(FlaskForm) con un campo por columna del CSV. Para las calificaciones (café, wifi, enchufes) es mejor un SelectField con las opciones fijas que un campo de texto libre: así nadie escribe algo que rompa el formato.

Requisito 7 — validar la URL (verificado con WTForms 3.2):

Valor ¿URL() lo acepta?
https://maps.app.goo.gl/abc ✅
maps.google.com (sin https://) ❌
http://localhost:5000 ❌ (exige un dominio con extensión, como .com)
javascript:alert(1) ❌

Requisito 8 — agregar al CSV. Hazlo con el módulo csv, no uniendo textos con comas (sección 3).

⚠️ Errores frecuentes

Error Causa Solución
La tabla muestra ['Cafe Name', 'Location', ...] Imprimiste la fila entera en lugar de recorrer sus celdas Ciclo anidado
Café o símbolos raros en la tabla Leíste el CSV sin encoding="utf-8" en Windows Sección 3
El formulario de /add no hace nada al enviarlo La ruta no acepta POST o no llamas a validate_on_submit() methods=["GET", "POST"]
El nuevo café aparece pegado al último en la misma línea El archivo no terminaba en salto de línea Sección 3
La ubicación con coma en el nombre rompe las columnas Uniste los datos con ",".join(...) csv.writer

3. 📖 CSV sin sorpresas: codificación, comas y saltos de línea

(Sección creada para esta adaptación a partir de los requisitos 2 y 8.)

🎯 Objetivo de aprendizaje

Leer y escribir un CSV de forma que funcione con acentos, emojis y comas dentro de los datos, en cualquier sistema operativo.

📖 Problema 1: la codificación en Windows (verificado)

En Windows, open() sin encoding usa la codificación regional (cp1252), no UTF-8. Resultado al leer un CSV guardado en UTF-8:

Contenido del archivo open("archivo.csv") en Windows open("archivo.csv", encoding="utf-8")
Café,☕☕☕ Café,☕☕☕ (texto dañado, sin error) Café,☕☕☕
Cafe,☕️ (emoji con selector de variante) UnicodeDecodeError: 'charmap' codec can't decode byte 0x8f Cafe,☕️

Regla: siempre encoding="utf-8" al leer y al escribir. En Mac y Linux el error no aparece, por eso un programa "que funciona en mi computador" falla en el del compañero.

📖 Problema 2: el salto de línea final (verificado)

Si el archivo no termina con un salto de línea y agregas "\nNuevo,...", y luego otra fila con csv.writer (que pone el salto al final de cada fila, no al principio), el resultado es:

Texto
Cafe,Location
Lighthaus,https://a
Otro,https://b"Café, con coma",https://c      ← dos registros en una línea

Regla: agrega filas siempre con csv.writer y asegúrate de que el archivo termine en salto de línea (ábrelo en un editor y revisa la última línea). Una forma robusta:

Python
import csv

def agregar_fila(ruta, fila):
    with open(ruta, "a", newline="", encoding="utf-8") as archivo:
        csv.writer(archivo).writerow(fila)

newline="" evita que en Windows aparezcan líneas en blanco entre filas.

📖 Problema 3: comas dentro de los datos

",".join(["Café, con coma", "https://c"]) produce Café, con coma,https://c: tres columnas en vez de dos. csv.writer lo resuelve poniendo comillas: "Café, con coma",https://c (verificado).

📖 Alternativas

Herramienta Ventaja Cuándo
Módulo csv En la biblioteca estándar; control total Proyectos pequeños como este
csv.DictReader / DictWriter Cada fila es un diccionario: fila["Location"] en vez de fila[1] Cuando el orden de columnas puede cambiar
pandas (Día 25) Filtrar, ordenar y calcular con poco código Análisis de datos
Base de datos (Día 63) Varios usuarios a la vez, búsquedas, integridad Cuando el sitio crece

Retos adicionales (creados para esta adaptación)

  • 🟢 Reto 1: reescribe la lectura con csv.DictReader y muestra la tabla usando los nombres de las columnas.
  • 🟡 Reto 2: en /cafes, agrega un filtro por la URL: /cafes?wifi=5 muestra solo las cafeterías con wifi máximo (request.args, Día 60).
  • 🟠 Reto 3: antes de agregar un café, comprueba que no exista ya uno con el mismo nombre (sin distinguir mayúsculas) y muestra un error en el formulario.
  • 🔴 Reto 4 — Pensamiento computacional: ¿qué pasa si dos personas envían el formulario al mismo tiempo? Investiga qué es una "condición de carrera" y explica por qué una base de datos (Día 63) lo maneja mejor que un CSV.

4. 🚀 Proyecto del día: Coffee & Wifi

(Proyecto original del curso: lecciones 440 y 442.)

Problema

Quien trabaja o estudia con portátil necesita saber qué cafeterías tienen buen café, wifi estable y enchufes. La información está en un CSV que nadie puede consultar cómodamente ni ampliar.

Contexto

En tu ciudad también pasa: bibliotecas públicas, cafés y parques con wifi. Un sitio colaborativo permite que la comunidad comparta esa información.

Objetivo

Completar el backend de Coffee & Wifi según los 9 requisitos del curso.

Requisitos funcionales

Los 9 requisitos de la sección 2.

Restricciones técnicas

  • Plantillas que heredan de base.html con Bootstrap-Flask.
  • Formulario con Flask-WTF, render_form y el validador URL().
  • Lectura y escritura del CSV con el módulo csv, newline="" y encoding="utf-8".
  • Enlaces internos con url_for.

Planificación

  1. Comprender (sección 1). 2. Entradas: el CSV y el formulario. 3. Procesos: leer → mostrar; validar → agregar. 4. Salidas: tabla y CSV actualizado. 5. Dividir en los 9 requisitos. 6. Diseñar la clase CafeForm a partir de las columnas del CSV. 7. Implementar. 8. Probar. 9. Corregir. 10. Mejorar.

Construcción paso a paso

  • Versión 1: portada con estilos (requisito 1) y botón "Show Me!" (requisito 4).
  • Versión 2: /cafes mostrando {{ cafes }} en un <p> para entender los datos.
  • Versión 3: tabla de Bootstrap con ciclos anidados y "Maps Link" (requisitos 2 y 3).
  • Versión 4: /add con el formulario y la validación de URL (requisitos 5 a 7).
  • Versión final: guardar en el CSV, redirigir a /cafes y revisar la navegación (requisitos 8 y 9).

Pruebas

Caso Resultado esperado
/ Portada con styles.css
"Show Me!" Lleva a /cafes
/cafes Tabla con todas las filas; la columna de ubicación dice "Maps Link"
/add con ubicación maps.google.com Error de validación; nada se guarda
/add válido La tabla muestra el café nuevo en su propia fila
Nombre con tilde y coma ("Café Juan, Centro") Se guarda entre comillas y se lee bien
Abrir cafe-data.csv en un editor tras agregar Una fila por línea, sin líneas en blanco

🧪 Prueba y depuración

Python
@app.route("/add")
def add_cafe():
    form = CafeForm()
    if form.validate_on_submit():
        with open("cafe-data.csv", "a") as f:
            f.write(f"{form.cafe.data},{form.location.data},{form.coffee_rating.data}")
        return redirect(url_for("cafes"))
    return render_template("add.html", form=form)

Encuentra cuatro problemas: la ruta no acepta POST; falta encoding="utf-8"; se escribe sin salto de línea (el siguiente café quedará pegado); y unir con comas rompe los nombres que contienen comas (usa csv.writer).

⭐ Extensión del proyecto

  • Ordenar la tabla por la calificación del wifi (en Python, con sorted y key).
  • Mostrar las calificaciones como insignias de Bootstrap (badge) con colores según el valor.
  • Una página /cafes/<int:indice> con el detalle de un café y el mapa incrustado.

5. 🌟 Proyecto extra: el mapa de lugares para estudiar en el barrio

(Proyecto creado para esta adaptación; no existe en el curso original.)

Situación

Muchos estudiantes no tienen buen Internet en casa. El grupo quiere recopilar, con ayuda de la comunidad, los lugares del barrio donde se puede estudiar: bibliotecas, casas de la cultura, parques con wifi, cafés.

Requisitos

  1. lugares.csv con: nombre, tipo (biblioteca, parque, café, otro), dirección aproximada, enlace al mapa, horario, wifi gratuito (sí/no), enchufes (sí/no), costo (gratis, consumo mínimo) y fecha de registro.
  2. /lugares con una tabla de Bootstrap y filtros por tipo y por "wifi gratuito" (request.args).
  3. /proponer con Flask-WTF: SelectField para el tipo y los sí/no, URL() para el mapa, validación del horario con Regexp (por ejemplo, 8:00-18:00).
  4. Las propuestas nuevas se guardan en pendientes.csv, no en lugares.csv: una ruta /revisar (sin enlace público) permite al docente aprobarlas.

Decisiones que debes tomar (y justificar)

  • ¿Por qué separar las propuestas pendientes de las aprobadas?
  • ¿Qué información no debe publicarse (por ejemplo, la dirección exacta de una casa)?

Casos de prueba

  • Filtro /lugares?tipo=biblioteca&wifi=si → solo bibliotecas con wifi gratuito.
  • Horario ocho a seis → error de formato.
  • Una propuesta aprobada pasa de pendientes.csv a lugares.csv y desaparece de la lista de pendientes.

6. 🏗️ Proyecto integrador del bloque avanzado — Etapa 4

(Proyecto creado para esta adaptación. Descripción completa en el tutorial del Día 70.)

La Plataforma del Club — Etapa 4 (hoy):

  1. /actividades muestra en una tabla de Bootstrap las actividades del club leídas de actividades.csv (fecha, nombre, lugar, enlace).
  2. /actividades/nueva (sin enlace público por ahora) con Flask-WTF, validadores (DataRequired, URL() para el enlace, fecha con DateField) y guardado con csv.writer.
  3. Lectura y escritura con encoding="utf-8" y newline="".
  4. Escribe en el README.md qué limitaciones tiene guardar los datos en CSV (las resolverás el Día 63).

7. 🧩 Comprueba lo aprendido

Conceptuales

  1. ¿Por qué el curso dice que este proyecto se parece al trabajo con un cliente real?
  2. ¿Por qué /add no tiene un botón visible? ¿Eso la hace segura?
  3. ¿Qué hace newline="" al abrir un CSV? ¿Y encoding="utf-8"?
  4. ¿Por qué csv.writer es mejor que unir los datos con comas?
  5. ¿Por qué URL() rechaza maps.google.com?

Análisis de código

HTML
{% for fila in cafes %}
  <tr>
  {% for celda in fila %}
    {% if loop.first %}<th>{{ celda }}</th>{% else %}<td>{{ celda }}</td>{% endif %}
  {% endfor %}
  </tr>
{% endfor %}

¿Qué celdas salen como <th>? ¿Es lo que querías para los encabezados?

Predicción

Python
import csv, io
salida = io.StringIO()
csv.writer(salida).writerow(["Café Juan, Centro", "https://x.co", "☕☕☕"])
print(salida.getvalue())

Depuración

Un compañero dice: "En mi Mac la tabla se ve bien, pero en el computador del colegio salen símbolos raros". ¿Cuál es la causa más probable y cómo la corriges?

Modificación

Cambia /cafes para que la tabla muestre solo las cafeterías con enchufes en todas las mesas (o el valor máximo de la columna de enchufes).

Creación

Escribe una función leer_csv_como_diccionarios(ruta) que devuelva una lista de diccionarios, y úsala para mostrar en una plantilla una tarjeta de Bootstrap por fila.


8. 🤖 Programar con apoyo de IA

Ciclo de trabajo: formula el problema → inténtalo tú → consulta a la IA → lee y entiende → prueba → compara con la documentación oficial → modifica → explica con tus palabras.

Actividad 1 — El CSV de la IA. Pide a una IA el código para agregar una fila a un CSV desde Flask. Revisa si usa encoding, newline y csv.writer. Pruébalo con un nombre que tenga tilde y coma.

Actividad 2 — Validar una URL. Pregunta a una IA si wtforms.validators.URL acepta http://localhost:5000. Compruébalo tú y revisa en la documentación de WTForms el parámetro require_tld.

Actividad 3 — "Ruta secreta". Pregunta a una IA si una ruta sin enlace, como /add, es segura. Compara su respuesta con lo que aprenderás el Día 68 (autenticación) y escribe tu conclusión.


9. 🛠️ Correcciones técnicas al material original

Archivo Afirmación o comportamiento del curso Observación Tratamiento
441 Lección "Look at the Desired Final Product" El archivo contiene la descripción publicitaria del curso, no la lección Matriz de trazabilidad; la demostración se describe a partir de 442
442 Agregar los datos al final del CSV, "separados por comas" Unir con comas rompe los datos que contienen comas; sin salto final, las filas se pegan (verificado) Sección 3
440, 442 Leer el CSV En Windows, sin encoding="utf-8", los acentos y emojis se dañan o producen UnicodeDecodeError (verificado) Sección 3
442 Ruta "secreta" /add No tener botón no la protege: cualquiera que conozca la URL puede entrar (se resuelve con autenticación, Día 68) Comprueba lo aprendido y actividad 3 de IA
442 Validar la URL con los validadores de WTForms 2.3 Funciona igual en WTForms 3.2; URL() rechaza direcciones sin esquema y localhost (verificado) Tabla del requisito 7

10. 📋 Matriz de trazabilidad

Contenido original Archivo de origen Concepto desarrollado Ejercicio original Reto adicional Proyecto relacionado
Archivos de inicio, entorno virtual, requirements.txt, CSV de cafeterías de Londres 440 Sección 1 Preparar el proyecto Leer el proyecto antes de programar Sección 4
(El archivo contiene la descripción comercial del curso; no aporta contenido para el tutorial) 441 — — — —
Frontend ya construido, objetivos del día, 9 requisitos (estilos, /cafes, tabla, "Maps Link", "Show Me!", /add, render_form, validación de URL, agregar al CSV, navegación), consejos para atascarse 442 Secciones 0 y 2 Proyecto del día Guía de razonamiento por requisito Sección 4
(Consecuencia de los requisitos 2 y 8; no se trata en el curso) — Sección 3 — Codificación, saltos de línea y comas; retos 1–4 Secciones 4, 5 y 6
(No existe en el curso original) — — — Lugares para estudiar en el barrio Sección 5
(No existe en el curso original) — — — Plataforma del Club, etapa 4 Sección 6

11. 📦 Evidencias de aprendizaje del Día 62

  • Coffee & Wifi con los 9 requisitos, con una lista de chequeo marcada y capturas.
  • El CSV después de agregar dos cafés (uno con tilde y coma), abierto en un editor de texto.
  • La tabla de pruebas del proyecto con sus resultados.
  • El mapa de lugares para estudiar (proyecto extra).
  • La etapa 4 de la Plataforma del Club.
  • Cuaderno de errores: fila impresa como lista, texto dañado por la codificación, UnicodeDecodeError, filas pegadas, líneas en blanco sin newline="", comas dentro de los datos, ruta sin POST, URL sin https://.