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 sinencoding="utf-8"en Windows produce texto dañado (Café→Café) oUnicodeDecodeErrorcon 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
- Antes de empezar: un proyecto como los de un cliente real
- El proyecto de inicio
- Los requisitos, uno por uno
- 📖 CSV sin sorpresas: codificación, comas y saltos de línea
- 🚀 Proyecto del día: Coffee & Wifi
- 🌟 Proyecto extra: el mapa de lugares para estudiar en el barrio
- 🏗️ Proyecto integrador del bloque avanzado — Etapa 4
- 🧩 Comprueba lo aprendido
- 🤖 Programar con apoyo de IA
- 🛠️ Correcciones técnicas al material original
- 📋 Matriz de trazabilidad
- 📦 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
- Descarga y descomprime los archivos de inicio.
- Ábrelos en PyCharm y acepta crear el entorno virtual e instalar
requirements.txt. Si hay subrayados rojos: File → Reload All from Disk. - Si no te lo ofrece, crea el entorno a mano (Add Local Interpreter, sin "inherit global site-packages").
- Si aún faltan paquetes:
python -m pip install -r requirements.txt(Windows) opip3 install -r requirements.txt(macOS). - Explora el proyecto: hay un archivo
.csvcon 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
- Portada con estilo: la página de inicio debe usar
css/styles.css. Pista: agrega abase.htmllos bloques de la plantilla de inicio de Bootstrap-Flask y enlaza la hoja de estilos. - Ruta
/cafes: debe renderizarcafes.htmlcon una tabla de Bootstrap con todos los datos decafe-data.csv. Pista: la ruta envía un objetocafes; ponlo dentro de un<p>para ver cómo es. - 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". - El botón "Show Me!" de la portada lleva a
cafes.html. - Ruta secreta
/add: no tiene botón, pero "quien la conozca" puede entrar; renderizaadd.html. - En
add.html, un formulario con WTForms yrender_formcon todos los campos de la demostración. - El campo de la ubicación debe validar que sea una URL. (El curso enlaza cómo desactivar la validación del navegador.)
- 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. - 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:
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 encabezadosEn 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:
{% 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:
Cafe,Location
Lighthaus,https://a
Otro,https://b"Café, con coma",https://c ← dos registros en una líneaRegla: 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:
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.DictReadery muestra la tabla usando los nombres de las columnas. - 🟡 Reto 2: en
/cafes, agrega un filtro por la URL:/cafes?wifi=5muestra 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.htmlcon Bootstrap-Flask. - Formulario con Flask-WTF,
render_formy el validadorURL(). - Lectura y escritura del CSV con el módulo
csv,newline=""yencoding="utf-8". - Enlaces internos con
url_for.
Planificación
- 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
CafeForma 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:
/cafesmostrando{{ 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:
/addcon el formulario y la validación de URL (requisitos 5 a 7). - Versión final: guardar en el CSV, redirigir a
/cafesy 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
@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
sortedykey). - 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
lugares.csvcon: 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./lugarescon una tabla de Bootstrap y filtros por tipo y por "wifi gratuito" (request.args)./proponercon Flask-WTF:SelectFieldpara el tipo y los sí/no,URL()para el mapa, validación del horario conRegexp(por ejemplo,8:00-18:00).- Las propuestas nuevas se guardan en
pendientes.csv, no enlugares.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.csvalugares.csvy 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):
/actividadesmuestra en una tabla de Bootstrap las actividades del club leídas deactividades.csv(fecha, nombre, lugar, enlace)./actividades/nueva(sin enlace público por ahora) con Flask-WTF, validadores (DataRequired,URL()para el enlace, fecha conDateField) y guardado concsv.writer.- Lectura y escritura con
encoding="utf-8"ynewline="". - Escribe en el
README.mdqué limitaciones tiene guardar los datos en CSV (las resolverás el Día 63).
7. 🧩 Comprueba lo aprendido
Conceptuales
- ¿Por qué el curso dice que este proyecto se parece al trabajo con un cliente real?
- ¿Por qué
/addno tiene un botón visible? ¿Eso la hace segura? - ¿Qué hace
newline=""al abrir un CSV? ¿Yencoding="utf-8"? - ¿Por qué
csv.writeres mejor que unir los datos con comas? - ¿Por qué
URL()rechazamaps.google.com?
Análisis de código
{% 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
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 sinnewline="", comas dentro de los datos, ruta sin POST, URL sinhttps://.