Saltar al contenido
Volver al inicio 100 Días de Código en Python — Día 60: Formularios HTML y solicitudes POST en Flask

100 Días de Código en Python — Día 60: Formularios HTML y solicitudes POST en Flask

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

Este tutorial adapta pedagógicamente el Día 60. Las secciones 📝 Ejercicio original del curso provienen de las lecciones 425 a 430; las soluciones oficiales están en los gists enlazados y los archivos de inicio y final en los recursos de las lecciones 429 y 430. Los retos 🟢🟡🟠🔴, las secciones sobre GET contra POST, validación y correo con tildes, el proyecto extra y la etapa 2 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, Flask 3.1.3 y un servidor de correo local (aiosmtpd), se comprobó con el cliente de pruebas de Flask: que un GET a una ruta solo POST responde 405, que falta un campo en request.form responde 400, el flujo GET/POST en una sola ruta con msg_sent, y que el envío de correo del estilo del Día 32 (sendmail con una cadena) falla con UnicodeEncodeError si el mensaje tiene tildes, mientras que EmailMessage + send_message funciona. En la plantilla Clean Blog 6.0.9 se comprobó que los campos del formulario de contacto no tienen atributo name y que el botón trae la clase disabled.

🏫 Para el aula: los correos se envían al servidor de correo local (python -m aiosmtpd -n -l localhost:8025, como en los Días 40 y 47). Así no se necesitan contraseñas reales y nada sale del computador.


Tabla de contenidos

  1. Antes de empezar: el formulario que no hacía nada
  2. Repaso: un formulario desde cero
  3. Recibir un POST en Flask
  4. 📖 GET contra POST
  5. El formulario de contacto del blog
  6. Enviar el correo con smtplib
  7. 🚀 Proyecto del día: el blog que recibe mensajes
  8. 🌟 Proyecto extra: el buzón de sugerencias del colegio
  9. 🏗️ Proyecto integrador del bloque avanzado — Etapa 2
  10. 🧩 Comprueba lo aprendido
  11. 🤖 Programar con apoyo de IA
  12. 🛠️ Correcciones técnicas al material original
  13. 📋 Matriz de trazabilidad
  14. 📦 Evidencias de aprendizaje del Día 60

0. Antes de empezar: el formulario que no hacía nada

(Fuente: 425-day-60-goals-make-the-contact-form-work.md)

📝 Contenido original del curso

El blog del Día 59 funciona completo… salvo el formulario de contacto. Para arreglarlo hay que aprender cómo se envía un formulario HTML y cómo recibir sus datos en Flask. El objetivo del día: que, al llenar el formulario, el dueño del sitio reciba un correo con el nombre, el correo, el teléfono y el mensaje del visitante.

Prerequisitos: formularios HTML (Días 41 y 42), el blog del Día 59, envío de correo con smtplib (Día 32), condicionales en Jinja (Día 57).

📖 Hasta ahora, solo "leías" (adaptación)

Todas tus rutas respondían a solicitudes para ver algo. Hoy, por primera vez, el navegador envía datos a tu servidor. Es el paso de un sitio "de solo lectura" a una aplicación que recibe información de las personas, con la responsabilidad que eso implica: validar lo que llega y cuidar esos datos.


1. Repaso: un formulario desde cero

(Fuente: 426-html-forms-revision-creating-a-form-from-scratch.md)

📝 Ejercicio original del curso

  1. Crea un proyecto html-forms con main.py y templates/index.html.
  2. Crea una aplicación Flask que sirva index.html, con un <h1> para comprobar que funciona.
  3. Reto: crea en index.html un formulario como el de la imagen del curso: un campo para el nombre, uno para la contraseña y un botón "Ok".

📖 Explicación conceptual

HTML
<form>
    <label for="username">Name</label>
    <input type="text" id="username">
    <label for="password">Password</label>
    <input type="password" id="password">
    <button type="submit">Ok</button>
</form>
  • type="password" oculta lo que se escribe (¡pero no lo cifra!).
  • <label for="..."> enlazado con el id del campo: al hacer clic en la etiqueta, el cursor va al campo, y los lectores de pantalla saben qué campo es. Es accesibilidad básica.

2. Recibir un POST en Flask

(Fuentes: 427-handle-post-requests-with-flask-servers.md y 428-post-requests-in-flask-solution.md)

🎯 Objetivo de aprendizaje

Hacer que un formulario envíe sus datos con POST a una ruta de Flask y leerlos con request.form.

📝 Ejercicio original del curso

Al pulsar "Ok", no pasa nada. Para que el formulario envíe datos necesita dos atributos:

  1. Reto: con la documentación de W3Schools sobre method y action, haz que el formulario envíe una solicitud POST a la ruta /login.
  2. Cada campo necesita un atributo name: es la "etiqueta" con la que el servidor reconoce cada dato.
  3. En main.py, una ruta que reciba el POST:
Python
   @app.route("/login", methods=["POST"])
   def receive_data():
       return "Success"

methods recibe una lista, así que una ruta puede atender varios métodos: @app.route("/contact", methods=["GET", "POST"]).

  1. Flask tiene un objeto request (¡no confundir con el módulo requests!) que da acceso a lo que llegó en la solicitud. 💪 Reto difícil: con la documentación de Flask (The Request Object), obtén el nombre y la contraseña y devuélvelos en un <h1>.

La solución (lección 428):

HTML
<form action="{{ url_for('receive_data') }}" method="post">
    <input type="text" name="username">
    <input type="password" name="password">
    <button type="submit">Ok</button>
</form>
Python
from flask import Flask, render_template, request

@app.route("/login", methods=["POST"])
def receive_data():
    name = request.form["username"]
    password = request.form["password"]
    return f"<h1>Name: {name}, Password: {password}</h1>"

El action puede ser "/login", pero "según dónde esté alojado tu servidor, esa ruta puede cambiar", así que es mejor generarla con url_for.

📖 Explicación conceptual

El viaje de los datos:

Texto
<input name="username"> ──(el usuario escribe "ana")──▶ POST /login   cuerpo: username=ana&password=123
                                                              │
                                              Flask ──▶ request.form = {"username": "ana", "password": "123"}
  • El name del HTML es la clave en request.form. El id no viaja al servidor.
  • request.form se comporta como un diccionario.

Comportamientos verificados con Flask 3.1:

Situación Respuesta de Flask
Visitar /login escribiendo la URL (GET) en una ruta con methods=["POST"] 405 Method Not Allowed
POST sin el campo password y el código usa request.form["password"] 400 Bad Request
POST sin el campo y el código usa request.form.get("password") Sigue funcionando; el valor es None

🛠️ Nota técnica: el reto devuelve la contraseña en pantalla solo para practicar. En una aplicación real nunca se muestra ni se guarda una contraseña tal como llega (el Día 68 aprenderás a guardarla con hash). Y como en el Día 55, devolver datos del usuario dentro de una f-string con HTML permite inyectar código: usa escape o, mejor, una plantilla.

⚠️ Errores frecuentes

Error Causa Solución
405 Method Not Allowed La ruta no acepta el método con el que llegó Agrega el método a methods=[...]
400 Bad Request (KeyError por debajo) El name del HTML no coincide con la clave que buscas, o falta Revisa que coincidan exactamente
NameError: name 'request' is not defined No lo importaste from flask import request
AttributeError: module 'requests' has no attribute 'form' Importaste requests (el de las API) Es request, sin s, desde flask
Los datos aparecen en la barra de direcciones El formulario no tiene method="post" (por defecto es GET) method="post"

Retos adicionales (creados para esta adaptación)

  • 🟢 Reto 1 — Aplicación: un formulario con nombre y color favorito que devuelva una página con fondo de ese color (usa <input type="color">).
  • 🟡 Reto 2 — Ampliación: cambia el formulario a method="get" y la ruta a methods=["GET"]; lee los datos con request.args. ¿Qué ves en la barra de direcciones?
  • 🟠 Reto 3 — Desafío: con request.form.get(...), muestra un mensaje amable si algún campo llega vacío, en lugar de un 400.
  • 🔴 Reto 4 — Pensamiento computacional: con las herramientas de desarrollo (pestaña Red), envía el formulario y encuentra la solicitud POST. ¿Dónde están los datos? ¿Viajan cifrados en http://127.0.0.1? ¿Qué cambiaría con https://?

3. 📖 GET contra POST

(Sección creada para esta adaptación a partir de las lecciones 427 a 429.)

GET POST
Para qué Pedir o ver algo Enviar datos para que el servidor haga algo
Dónde van los datos En la URL: /buscar?q=cactus En el cuerpo de la solicitud
Se puede guardar en marcadores o compartir Sí No
Queda en el historial del navegador Sí, con los datos La URL sí; los datos no
Ejemplos Buscar, filtrar, ver una entrada Iniciar sesión, enviar un mensaje, publicar
En Flask request.args request.form

Regla práctica: si la acción cambia algo (envía un correo, guarda un registro) o lleva datos privados, usa POST.

💡 POST no es "seguro" por sí solo: los datos no se ven en la URL, pero viajan legibles salvo que el sitio use HTTPS. Por eso los sitios reales con formularios usan siempre https://.


4. El formulario de contacto del blog

(Fuente: 429-getting-the-contact-form-to-work.md)

🎯 Objetivo de aprendizaje

Atender la vista del formulario y su envío en una sola ruta, y mostrar un mensaje de confirmación con Jinja.

📝 Ejercicio original del curso

  1. Descarga los archivos de inicio: son el blog del Día 59 con un contact.html simplificado para que sea más fácil conectarlo con main.py.
  2. Agrega a main.py una ruta "/form-entry" que reciba los datos.
  3. Reto: que el servidor imprima en la terminal lo que el usuario escribió y devuelva un <h1> "Successfully sent your message".
  4. Reto: combina /contact y /form-entry en una sola ruta /contact que haga algo distinto según el método (GET o POST). Pistas: cambia el action de contact.html y usa request.method.
  5. Reto: en lugar de devolver un <h1> suelto, haz que el propio <h1> de contact.html diga "Successfully sent message". Pista: {% if %} de Jinja.

📖 Explicación conceptual

🛠️ Nota técnica (verificado): en el contact.html original de Clean Blog, los campos tienen id pero no name, el formulario depende de SB Forms (data-sb-form-api-token) y el botón tiene la clase disabled. Por eso el curso entrega uno simplificado. Si trabajas con el original, debes: quitar data-sb-form-api-token y el script sb-forms-latest.js, agregar action y method="post" al <form>, agregar name a cada campo y quitar disabled del botón.

La solución combinada (verificada):

Python
@app.route("/contact", methods=["GET", "POST"])
def contact():
    if request.method == "POST":
        data = request.form
        print(data["name"], data["email"], data["phone"], data["message"])
        return render_template("contact.html", msg_sent=True)
    return render_template("contact.html", msg_sent=False)
HTML
<form action="{{ url_for('contact') }}" method="post">
    ...
</form>
HTML
{% if msg_sent %}
    <h1>Successfully sent your message</h1>
{% else %}
    <h1>Contact Me</h1>
{% endif %}
  • La misma URL muestra el formulario (GET) y lo procesa (POST).
  • msg_sent es un booleano que decide qué título mostrar.

📖 Validar lo que llega (adaptación)

El atributo required en el HTML impide enviar campos vacíos… desde el navegador. Pero cualquiera puede enviar un POST sin pasar por tu formulario (con requests.post, Día 37). La validación real se hace en el servidor:

Python
nombre = request.form.get("name", "").strip()
if not nombre:
    return render_template("contact.html", msg_sent=False, error="Escribe tu nombre")

El Día 61 verás una herramienta que hace esto de forma ordenada: WTForms.

📖 Un detalle profesional: POST/Redirect/GET (adaptación)

Si después de enviar el formulario presionas F5, el navegador pregunta si quieres reenviar los datos: ¡se mandaría otro correo! La solución habitual es redirigir después de un POST exitoso:

Python
from flask import redirect, url_for

    if request.method == "POST":
        ...
        return redirect(url_for("contact", enviado=1))

La recarga ya no repite el envío. (redirect lo usarás mucho desde el Día 63.)

⚠️ Errores frecuentes

Error Causa Solución
405 al enviar La ruta solo acepta GET methods=["GET", "POST"]
El <h1> nunca cambia Olvidaste enviar msg_sent o lo escribiste distinto Mismo nombre en render_template y en la plantilla
if request.method == "post": nunca se cumple Flask entrega el método en mayúsculas "POST"
Al recargar se envía otro mensaje Reenvío del formulario POST/Redirect/GET

5. Enviar el correo con smtplib

(Fuente: 430-sending-email-with-smtplib.md)

📝 Ejercicio original del curso

Con lo aprendido en el Día 32, haz que el formulario de contacto envíe un correo al dueño del sitio con los datos del visitante. Puede que necesites repasar las lecciones de smtplib.

📖 Explicación conceptual

La forma del Día 32 construye el mensaje como una cadena y usa sendmail:

Python
def send_email(name, email, phone, message):
    email_message = f"Subject:New Message\n\nName: {name}\nEmail: {email}\nPhone: {phone}\nMessage:{message}"
    with smtplib.SMTP(SERVIDOR, PUERTO) as connection:
        connection.sendmail(REMITENTE, DESTINATARIO, email_message)

🛠️ Nota técnica (verificado con Python 3.14): si el visitante escribe una sola tilde o una ñ (por ejemplo, "María" o "¿Qué tal?"), sendmail falla:

Texto
UnicodeEncodeError: 'ascii' codec can't encode character '\xed' in position 32

porque convierte la cadena a ASCII. En español esto ocurre casi siempre. La solución es construir el correo con la clase EmailMessage de la biblioteca estándar, que codifica el asunto y el cuerpo en UTF-8 (verificado: el asunto "Nuevo mensaje de María" llega como =?utf-8?q?Mar=C3=ADa?= y se muestra bien):

Python
import os
import smtplib
from email.message import EmailMessage

SERVIDOR = os.environ.get("SMTP_SERVIDOR", "localhost")
PUERTO = int(os.environ.get("SMTP_PUERTO", "8025"))
DESTINATARIO = os.environ.get("CORREO_DUENO", "dueno@blog.local")


def send_email(name, email, phone, message):
    correo = EmailMessage()
    correo["Subject"] = f"Nuevo mensaje de {name}"
    correo["From"] = "formulario@blog.local"
    correo["To"] = DESTINATARIO
    correo["Reply-To"] = email          # al responder, le escribes al visitante
    correo.set_content(f"Nombre: {name}\nCorreo: {email}\nTeléfono: {phone}\n\n{message}")
    with smtplib.SMTP(SERVIDOR, PUERTO) as conexion:
        conexion.send_message(correo)
  • Con el servidor local de aiosmtpd no hacen falta starttls() ni login(). Con Gmail u otro proveedor real sí (Día 32), y la contraseña de aplicación va en un .env, nunca en el código.
  • Reply-To evita un error común: poner el correo del visitante en From. Muchos servidores rechazan correos cuyo From no les pertenece.

⚠️ Errores frecuentes

Error Causa Solución
UnicodeEncodeError: 'ascii' codec... sendmail con tildes EmailMessage + send_message
ConnectionRefusedError El servidor de correo local no está corriendo Ábrelo en otra terminal antes de probar
SMTPAuthenticationError (con Gmail) Contraseña normal en lugar de contraseña de aplicación Día 32; guárdala en .env
El asunto aparece en el cuerpo Falta la línea en blanco después de Subject: en la forma antigua Con EmailMessage no pasa
La página tarda y luego muestra error 500 Fallo del correo sin manejar try/except smtplib.SMTPException y un mensaje amable

Retos adicionales (creados para esta adaptación)

  • 🟢 Reto 1: envía también una copia de confirmación al visitante ("Recibimos tu mensaje").
  • 🟡 Reto 2: si el correo falla, muestra "No pudimos enviar tu mensaje, inténtalo más tarde" en lugar de un error 500.
  • 🟠 Reto 3: guarda cada mensaje también en mensajes.csv (fecha, nombre, correo, mensaje), por si el correo falla.
  • 🔴 Reto 4: evita el spam: agrega un campo oculto (honeypot) que las personas no ven; si llega con texto, es un robot y no se envía nada. Explica por qué funciona.

6. 🚀 Proyecto del día: el blog que recibe mensajes

(Integra las lecciones 425 a 430.)

Problema

El formulario de contacto del blog no envía nada: los lectores no tienen cómo comunicarse con el autor.

Objetivo

Que /contact muestre el formulario (GET), lo procese (POST), envíe un correo con los datos y confirme el envío en la misma página.

Requisitos funcionales

  1. Una sola ruta /contact con GET y POST.
  2. Campos con name: name, email, phone, message.
  3. Validación en el servidor: nombre, correo y mensaje obligatorios.
  4. Correo con EmailMessage al servidor local (aiosmtpd).
  5. Título "Mensaje enviado" o mensaje de error según el caso.

Restricciones técnicas

  • action con url_for.
  • Configuración del correo en variables de entorno o .env, con valores por defecto para el servidor local.
  • Ningún error 500 en los casos de prueba.

Planificación

  1. Comprender. 2. Entradas: los 4 campos. 3. Procesos: validar → enviar correo → confirmar. 4. Salidas: la página y el correo. 5. Dividir: ruta, validación, send_email. 6. Diseñar la tabla de casos. 7. Implementar. 8. Probar. 9. Corregir. 10. Mejorar.

Construcción paso a paso

  • Versión 1: /form-entry que imprime los datos (paso 3 del curso).
  • Versión 2: ruta única /contact con request.method.
  • Versión 3: msg_sent en la plantilla.
  • Versión 4: validación en el servidor.
  • Versión final: correo con EmailMessage y manejo de errores.

Pruebas

(Con python -m aiosmtpd -n -l localhost:8025 corriendo en otra terminal.)

Caso Resultado esperado
GET /contact Formulario y título "Contact Me"
POST completo Título de confirmación y el correo aparece en la terminal de aiosmtpd
POST con "María" y "¿Qué tal?" Se envía sin UnicodeEncodeError
POST sin mensaje Aviso de error; no se envía correo
POST con aiosmtpd apagado Mensaje amable, no un error 500
Recargar después de enviar (con PRG) No se envía un segundo correo

🧪 Prueba y depuración

Python
import requests
from flask import Flask, render_template

@app.route("/contact")
def contact():
    if requests.method == "post":
        send_email(requests.form["name"])
        return render_template("contact.html", msg_send=True)
    return render_template("contact.html")

Encuentra cuatro problemas: requests en lugar de request (y sin importarlo de flask), la ruta no acepta POST, el método se compara en minúsculas y la variable se llama msg_send pero la plantilla usa msg_sent.

⭐ Extensión del proyecto

  • Mostrar los datos del visitante en la página de confirmación ("Gracias, María").
  • Un campo "asunto" con una lista desplegable (<select>) que cambie el asunto del correo.

7. 🌟 Proyecto extra: el buzón de sugerencias del colegio

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

Situación

El consejo estudiantil quiere un buzón de sugerencias en línea, con opción de enviar anónimamente.

Requisitos

  1. Formulario con categoría (<select>: Infraestructura, Convivencia, Académico, Otro), sugerencia (<textarea>) y una casilla "Enviar de forma anónima" (<input type="checkbox">). Si no es anónimo, pide nombre y curso.
  2. Validación en el servidor: sugerencia de 20 a 500 caracteres; nombre obligatorio si no es anónimo.
  3. Cada sugerencia se guarda en sugerencias.csv y se envía por correo (servidor local) al consejo.
  4. Una página /estadisticas (GET) que muestre cuántas sugerencias hay por categoría.

Decisiones que debes tomar (y justificar)

  • ¿Qué significa realmente anónimo? ¿Guardas la hora exacta? ¿La dirección IP (request.remote_addr)?
  • Una casilla sin marcar no se envía en el formulario. ¿Cómo lo detectas en request.form?

Casos de prueba

  • Sugerencia anónima: el CSV y el correo no contienen nombre.
  • Sugerencia de 10 caracteres: error y nada se guarda.
  • /estadisticas refleja las sugerencias guardadas.

8. 🏗️ Proyecto integrador del bloque avanzado — Etapa 2

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

La Plataforma del Club — Etapa 2 (hoy):

  1. /contacto con GET y POST, validación en el servidor y correo con EmailMessage al servidor local.
  2. Un formulario /inscripcion para interesados en unirse al club (nombre, curso, interés), guardado en inscripciones.csv.
  3. POST/Redirect/GET en ambos formularios.
  4. Documenta en el README.md cómo arrancar el servidor de correo local.

9. 🧩 Comprueba lo aprendido

Conceptuales

  1. ¿Qué hacen los atributos action y method de un formulario?
  2. ¿Qué papel cumple el atributo name? ¿Viaja el id al servidor?
  3. ¿Qué diferencia hay entre request y requests?
  4. ¿Cuándo usarías GET y cuándo POST?
  5. ¿Por qué no basta con required en el HTML?

Análisis de código

Python
@app.route("/buscar")
def buscar():
    termino = request.args.get("q", "")
    return f"Buscaste: {escape(termino)}"

¿Qué muestra /buscar?q=cactus? ¿Y /buscar?

Predicción

Una ruta tiene methods=["POST"]. ¿Qué código de estado ves si escribes su URL en el navegador? ¿Y si envías el formulario sin uno de los campos que el código lee con corchetes?

Depuración

HTML
<form action="/contact" method="post">
  <input type="text" id="name">
  <button type="submit">Enviar</button>
</form>

El servidor responde 400 al enviar. ¿Por qué?

Modificación

Cambia el formulario de /login para que, si la contraseña tiene menos de 8 caracteres, la página muestre un aviso en lugar de los datos.

Creación

Crea una calculadora de IMC: un formulario con peso y estatura que, con POST, muestre el resultado en la misma página ({% if %}), validando que ambos sean números positivos.


10. 🤖 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 correo con tildes. Pide a una IA una función para enviar un correo con smtplib. Pruébala con "María" y "¿Qué tal?". Si falla, muéstrale el error y compara su corrección con la sección 5 y con la documentación de email.message.

Actividad 2 — Validación. Pide a una IA que valide el formulario de contacto. ¿Valida en el navegador, en el servidor o en ambos? Pruébalo enviando un POST con requests.post sin pasar por el formulario.

Actividad 3 — Comprobar una afirmación. Pregunta a una IA si POST es "más seguro" que GET. Contrasta su respuesta con la sección 3 y con la documentación de MDN sobre métodos HTTP.


11. 🛠️ Correcciones técnicas al material original

Archivo Afirmación o comportamiento del curso Observación Tratamiento
430 Enviar el correo como en el Día 32 (sendmail con una cadena) Falla con UnicodeEncodeError si hay tildes o ñ (verificado) Sección 5 con EmailMessage
427, 428 Devolver en pantalla el nombre y la contraseña Aceptable como ejercicio; en producción no se muestran ni se guardan contraseñas, y el HTML con datos del usuario debe escaparse Nota técnica de la sección 2
429 El contact.html del Día 59 no se conecta fácilmente Los campos originales no tienen name, el formulario usa SB Forms y el botón es disabled (verificado) Nota técnica de la sección 4
429 Validación del formulario El curso no valida en el servidor Sección 4: validar lo que llega
429 Responder con la misma página tras el POST Recargar reenvía el formulario Sección 4: POST/Redirect/GET

12. 📋 Matriz de trazabilidad

Contenido original Archivo de origen Concepto desarrollado Ejercicio original Reto adicional Proyecto relacionado
Formulario de contacto que no funciona, enviar datos y correo 425 Sección 0 — — Sección 6
Proyecto html-forms, servir index.html, reto del formulario 426 Sección 1 Formulario desde cero Accesibilidad con <label> Sección 6
action y method, atributo name, ruta con methods, objeto request, reto de mostrar nombre y contraseña 427 Sección 2 Retos 1–4 de la lección Retos 1–4 de la sección 2 Secciones 6 y 7
Solución con request.form, action con url_for 428 Sección 2 Solución Tabla de 405 y 400 Sección 6
(Síntesis de 427 a 429) — Sección 3 — GET contra POST Secciones 6 y 7
Archivos de inicio, /form-entry, imprimir datos, combinar rutas con request.method, {% if %} en el <h1> 429 Sección 4 Retos 3–5 de la lección Validación en el servidor; POST/Redirect/GET Secciones 6 y 8
Enviar el correo con smtplib (repaso del Día 32) 430 Sección 5 Correo del formulario Retos 1–4 de la sección 5; EmailMessage Secciones 6, 7 y 8
(No existe en el curso original) — — — Buzón de sugerencias Sección 7
(No existe en el curso original) — — — Plataforma del Club, etapa 2 Sección 8

13. 📦 Evidencias de aprendizaje del Día 60

  • El blog con el formulario de contacto funcionando y la captura del correo en la terminal de aiosmtpd (con un mensaje que tenga tildes).
  • La captura de la pestaña Red con la solicitud POST y sus datos (Reto 4 de la sección 2).
  • La tabla de pruebas del proyecto del día con sus resultados.
  • El buzón de sugerencias (proyecto extra).
  • La etapa 2 de la Plataforma del Club.
  • Cuaderno de errores: 405, 400, request contra requests, campos sin name, método en minúsculas, variable con otro nombre en la plantilla, reenvío al recargar, UnicodeEncodeError en el correo, servidor de correo apagado.