Saltar al contenido
Volver al inicio 100 Días de Código en Python — Día 61: Formularios avanzados con Flask-WTF, herencia de plantillas y Bootstrap-Flask

100 Días de Código en Python — Día 61: Formularios avanzados con Flask-WTF, herencia de plantillas y Bootstrap-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 61 (nivel avanzado).

Este tutorial adapta pedagógicamente el Día 61. Las secciones 📝 Ejercicio original del curso provienen de las lecciones 431 a 439; las soluciones oficiales están en los gists enlazados y los archivos de inicio y final (day-61-starting-files-flask-secrets.zip, day-61-flask-secrets-end.zip) en los recursos. En varias lecciones el código aparece solo en imágenes; aquí se reconstruye a partir del texto y de la documentación oficial, y se indica. Los retos 🟢🟡🟠🔴, las secciones sobre entornos virtuales, CSRF y la clave secreta, el proyecto extra y la etapa 3 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, Flask-WTF 1.3.0, WTForms 3.2.2 y Bootstrap-Flask 2.6.0, se ejecutó el proyecto "secretos" completo con el cliente de pruebas de Flask. Se comprobó: que Email() sin el paquete email_validator provoca un error 500; los mensajes de error de DataRequired, Email y Length; que sin el token CSRF el formulario no se valida; la herencia con {{ super() }}; y que bootstrap.load_css() de Bootstrap-Flask carga Bootstrap 5.3.8.


Tabla de contenidos

  1. Antes de empezar: ¿por qué otra forma de hacer formularios?
  2. Instalar Flask-WTF: entornos virtuales y requirements.txt
  3. Crear formularios con clases
  4. Mejoras al código del formulario
  5. Validación
  6. Recibir los datos del formulario
  7. Herencia de plantillas con Jinja
  8. Bootstrap-Flask y render_form
  9. 🚀 Proyecto del día: la página de los secretos
  10. 🌟 Proyecto extra: la inscripción a la salida pedagógica
  11. 🏗️ Proyecto integrador del bloque avanzado — Etapa 3
  12. 🧩 Comprueba lo aprendido
  13. 🤖 Programar con apoyo de IA
  14. 🛠️ Correcciones técnicas al material original
  15. 📋 Matriz de trazabilidad
  16. 📦 Evidencias de aprendizaje del Día 61

0. Antes de empezar: ¿por qué otra forma de hacer formularios?

(Fuente: 431-day-61-goals-building-advanced-forms.md)

📝 Contenido original del curso

Ayer hiciste funcionar un formulario HTML con Flask. Hoy lo mejoras con la extensión Flask-WTF, que tiene varias ventajas:

  • Validación fácil: comprueba que el usuario escriba los datos en el formato correcto (por ejemplo, que un correo tenga "@" y "."), sin escribir tu propio código de validación.
  • Menos código: si el sitio tiene varios formularios, reduce mucho lo que hay que escribir o copiar.
  • Protección CSRF incorporada: CSRF (Cross-Site Request Forgery) es un ataque que obliga a tus usuarios a hacer acciones que no querían (por ejemplo, transferir dinero a un desconocido) o compromete el sitio si la víctima es administradora.

Los desarrolladores de Flask suelen elegir Flask-WTF, pero en la práctica también verás proyectos con formularios HTML simples: hay que entender ambos.

Lo que construirás: un sitio con una página de secretos a la que solo se entra con el usuario y la contraseña correctos.

Prerequisitos: formularios y POST en Flask (Día 60), clases y herencia (Días 16 a 21), {% include %} (Día 59), Bootstrap (Día 58).

📖 Qué es CSRF, con un ejemplo (adaptación)

Imagina que iniciaste sesión en la plataforma de notas del colegio. Luego visitas otra página, maliciosa, que tiene escondido un formulario que se envía solo hacia plataforma-notas/cambiar-correo. Tu navegador envía la solicitud con tu sesión abierta, y la plataforma cree que fuiste tú.

La defensa: el servidor pone en cada formulario un código secreto de un solo uso (token CSRF). La página maliciosa no puede conocerlo, así que su envío falso es rechazado. Flask-WTF lo hace por ti.


1. Instalar Flask-WTF: entornos virtuales y requirements.txt

(Fuente: 432-installing-flask-wtf.md)

🎯 Objetivo de aprendizaje

Crear un entorno virtual para un proyecto e instalar sus dependencias desde requirements.txt.

📝 Contenido original del curso

  1. Descarga y descomprime los archivos de inicio.
  2. Ábrelos en PyCharm. Puede que te ofrezca crear un entorno virtual e instalar las dependencias de requirements.txt: acepta. Si siguen apareciendo subrayados rojos: File → Reload All from Disk.
  3. Si PyCharm no lo ofrece, crea el entorno a mano: File → Settings → Project → Python Interpreter → Add Interpreter → Add Local Interpreter, con las opciones por defecto y sin marcar "inherit global site-packages". Se crea una carpeta venv con los paquetes del proyecto, aislados del sistema y de otros proyectos: "la configuración ideal para todos los proyectos de Python".

requirements.txt lista los paquetes (y sus versiones) de los que depende el proyecto. Así puedes compartir el proyecto sin los paquetes instalados: quien lo descargue los instala con:

Terminal
python -m pip install -r requirements.txt   # Windows
pip3 install -r requirements.txt            # macOS

Un paquete suelto: pip install Flask-WTF.

Reto: crea la ruta /login que renderice login.html.

📖 Lo mismo desde la terminal, en cualquier editor (adaptación)

Terminal
python -m venv venv                     # crea el entorno (una vez por proyecto)
venv\Scripts\activate                   # Windows (PowerShell: venv\Scripts\Activate.ps1)
source venv/bin/activate                # macOS / Linux
python -m pip install -r requirements.txt
python -m pip freeze > requirements.txt # guarda las versiones que usas (para compartir)
  • Con el entorno activo, la terminal muestra (venv) al inicio.
  • La carpeta venv no se comparte ni se sube a GitHub (lo verás en el Día 70 con .gitignore); se comparte requirements.txt.
  • python -m pip asegura que instalas en el mismo Python que ejecuta el proyecto.

⚠️ Errores frecuentes

Error Causa Solución
ModuleNotFoundError: No module named 'flask_wtf' Instalaste en otro Python o sin activar el entorno Activa el entorno; python -m pip install ...
El paquete se llama Flask-WTF pero se importa flask_wtf Nombres distintos en pip y en Python Normal: revisa la documentación
PowerShell no deja activar el entorno ("la ejecución de scripts está deshabilitada") Política de ejecución Set-ExecutionPolicy -Scope CurrentUser RemoteSigned, o usa venv\Scripts\python.exe directamente

2. Crear formularios con clases

(Fuente: 433-creating-forms-with-flask-wtf.md)

🎯 Objetivo de aprendizaje

Definir un formulario como una clase de Python y renderizarlo en una plantilla sin escribir <input> a mano.

📝 Ejercicio original del curso

Reto: con el Quickstart de Flask-WTF (Creating Forms), crea un formulario de inicio de sesión:

  • con los campos email y password;
  • ambos pueden ser StringField;
  • sin preocuparte todavía por los validadores;
  • ambos con tamaño 30 (el ancho del campo);
  • sin escribir ningún <label> ni <input> a mano en el HTML.

Pista: para la protección CSRF, agrega {{ form.csrf_token }} en login.html y crea una clave secreta en main.py (app.secret_key = "some secret string"), con la que se genera el token.

Nota del curso: necesitas dominar clases, objetos y herencia (Día 16); si la documentación te resulta difícil, es buen momento para repasar.

📖 Explicación conceptual

Python
from flask_wtf import FlaskForm
from wtforms import StringField


class LoginForm(FlaskForm):          # hereda de FlaskForm
    email = StringField("Email")
    password = StringField("Password")


@app.route("/login")
def login():
    login_form = LoginForm()
    return render_template("login.html", form=login_form)
HTML
<form method="POST" action="/login">
    {{ form.csrf_token }}
    {{ form.email.label }} {{ form.email(size=30) }}
    {{ form.password.label }} {{ form.password(size=30) }}
    <input type="submit" value="Log In">
</form>
  • Cada atributo de clase es un campo del formulario.
  • En la plantilla, form.email.label genera el <label> y form.email(size=30) genera el <input> (los argumentos se convierten en atributos HTML).
  • {{ form.csrf_token }} genera un <input type="hidden"> con el token.

📖 La clave secreta (adaptación)

app.secret_key se usa para firmar el token CSRF (y, desde el Día 68, las sesiones de usuario). Si alguien la conoce, puede falsificar esas firmas. Por eso:

  • en ejercicios, cualquier texto sirve;
  • en un proyecto real, se genera al azar (python -c "import secrets; print(secrets.token_hex(32))") y se guarda en .env, nunca en el código ni en GitHub.

⚠️ Errores frecuentes

Error Causa Solución
RuntimeError: A secret key is required to use CSRF. Falta app.secret_key Defínela antes de crear formularios
El formulario "no hace nada" al enviarlo Falta {{ form.csrf_token }} en la plantilla: la validación falla en silencio (verificado) Agrégalo dentro del <form>
AttributeError: 'LoginForm' object has no attribute 'correo' En la plantilla usas un nombre de campo distinto al de la clase Mismos nombres en la clase y en la plantilla

3. Mejoras al código del formulario

(Fuente: 434-code-improvements-for-our-wtforms.md; el código de esta lección aparece en imágenes y se reconstruye con la documentación de WTForms.)

📝 Contenido original del curso

La solución anterior es lo que "la mayoría puede escribir leyendo el Quickstart". Leyendo la documentación completa de WTForms se puede mejorar:

  1. Usar PasswordField para la contraseña: oculta lo que se escribe.
  2. Escribir el argumento label= explícitamente cuando no es obvio para qué sirve.
  3. Usar una URL dinámica en el action (url_for) en lugar de "/" fijo.
  4. Ordenar etiquetas y campos con elementos HTML normales (por ejemplo, un <p> por campo).
  5. Usar SubmitField para el botón.
Python
from wtforms import StringField, PasswordField, SubmitField


class LoginForm(FlaskForm):
    email = StringField(label="Email")
    password = PasswordField(label="Password")
    submit = SubmitField(label="Log In")
HTML
<form method="POST" action="{{ url_for('login') }}">
    {{ form.csrf_token }}
    <p>{{ form.email.label }} <br> {{ form.email(size=30) }}</p>
    <p>{{ form.password.label }} <br> {{ form.password(size=30) }}</p>
    {{ form.submit }}
</form>

4. Validación

(Fuente: 435-adding-validation-to-forms-with-flask-wtf.md)

🎯 Objetivo de aprendizaje

Agregar validadores a los campos, mostrar sus errores y desactivar la validación del navegador.

📝 Contenido original del curso

La validación incorporada es una de las mayores razones para usar WTForms: en lugar de programar reglas ("el correo debe tener @ y .", "la contraseña, mínimo 8 caracteres"), se usan validadores listos.

  1. Los validadores se pasan al crear cada campo, en una lista:
Python
   email = StringField(label="Email", validators=[DataRequired()])

DataRequired obliga a escribir algo. Si hay errores, WTForms genera una lista de errores en form.<campo>.errors.

  1. Se recorren para mostrarlos:
HTML
   {{ form.email(size=30) }}
   {% for err in form.email.errors %}
       <span style="color:red">{{ err }}</span>
   {% endfor %}
  1. La ruta debe aceptar POST y llamar a validate_on_submit():
Python
   @app.route("/login", methods=["GET", "POST"])
   def login():
       login_form = LoginForm()
       login_form.validate_on_submit()
       return render_template("login.html", form=login_form)
  1. Al probar, Chrome muestra su propio globo de "Completa este campo": no viene de WTForms sino del navegador, y cambia entre navegadores (y algunos no validan nada). Para que todos los usuarios vean tu validación, se desactiva la del navegador con el atributo novalidate en el <form>.

Reto: con la documentación de validadores, agrega Email al correo y Length (mínimo 8) a la contraseña. Pista: fíjate si la documentación pide instalar un paquete adicional para validar correos.

📖 Explicación conceptual

Python
from wtforms.validators import DataRequired, Email, Length


class LoginForm(FlaskForm):
    email = StringField(label="Email", validators=[DataRequired(), Email()])
    password = PasswordField(label="Password", validators=[DataRequired(), Length(min=8)])
    submit = SubmitField(label="Log In")

🛠️ Nota técnica (verificado): la pista del curso es clave. Email() necesita el paquete email_validator. Sin él, la página carga bien, pero al enviar el formulario aparece un error 500: Exception: Install 'email_validator' for email validation support. Solución: python -m pip install email_validator (y agrégalo a requirements.txt).

Mensajes que produce WTForms 3.2 (verificado):

Entrada Mensajes
Ambos campos vacíos This field is required. (en cada uno)
adminemail.com y 123 Invalid email address. y Field must be at least 8 characters long.

Los mensajes se pueden traducir: Length(min=8, message="Mínimo 8 caracteres").

Validación en dos lugares (repaso del Día 60): la del navegador (o required) es comodidad para el usuario; la del servidor (WTForms) es la seguridad, porque no se puede saltar.

Retos adicionales (creados para esta adaptación)

  • 🟢 Reto 1 — Aplicación: traduce al español los mensajes de los tres validadores.
  • 🟡 Reto 2 — Ampliación: agrega un campo "Confirmar contraseña" con el validador EqualTo("password").
  • 🟠 Reto 3 — Desafío: escribe un validador propio (una función def sin_espacios(form, field): que lance ValidationError) para impedir contraseñas con espacios.
  • 🔴 Reto 4 — Pensamiento computacional: diseña los validadores de un formulario de registro para la biblioteca escolar: nombre, correo institucional (debe terminar en el dominio del colegio), curso (de 6.° a 11.°) y código de estudiante de 6 dígitos. Escribe la tabla de casos de prueba antes del código.

5. Recibir los datos del formulario

(Fuente: 436-receiving-form-data-with-wtforms.md)

📝 Ejercicio original del curso

Con WTForms, obtener lo que escribió el usuario es aún más fácil: <objeto_formulario>.<campo>.data (por ejemplo, login_form.email.data). En lugar de if request.method == "POST", se usa el valor de validate_on_submit(): es True solo si el formulario se envió y pasó la validación.

Reto: en la ruta /login, si el formulario se envió, es válido y las credenciales son

  • correo: admin@email.com
  • contraseña: 12345678

muestra success.html; si no, denied.html.

📖 Explicación conceptual

Python
@app.route("/login", methods=["GET", "POST"])
def login():
    login_form = LoginForm()
    if login_form.validate_on_submit():
        if login_form.email.data == "admin@email.com" and login_form.password.data == "12345678":
            return render_template("success.html")
        return render_template("denied.html")
    return render_template("login.html", form=login_form)

Los tres caminos (verificados):

Situación validate_on_submit() Página
GET (primera visita) False login.html vacío
POST con errores de validación False login.html con los mensajes de error
POST válido con credenciales correctas True success.html
POST válido con credenciales incorrectas True denied.html

🛠️ Nota técnica: comparar con un correo y una contraseña escritos en el código está bien para practicar. En una aplicación real las contraseñas se guardan con hash en una base de datos (Día 68) y nunca aparecen en el código.


6. Herencia de plantillas con Jinja

(Fuente: 437-inheriting-templates-using-jinja2.md)

🎯 Objetivo de aprendizaje

Crear una plantilla base con bloques, extenderla en las páginas y reutilizar el contenido del padre con super().

📝 Contenido original del curso

{% include %} (Día 59) sirve cuando el encabezado y el pie son idénticos. Pero si quieres el mismo diseño en todo el sitio y poder cambiar partes en cada página, es mejor la herencia de plantillas, parecida a la herencia de clases: una plantilla padre con "huecos" (bloques) que las páginas hijas rellenan.

HTML
<!-- base.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>{% block title %}{% endblock %}</title>
</head>
<body>
    {% block content %}{% endblock %}
</body>
</html>
HTML
<!-- success.html -->
{% extends "base.html" %}
{% block title %}Success{% endblock %}
{% block content %}
   <div class="container">
      <h1>Top Secret</h1>
      <iframe src="https://giphy.com/embed/Ju7l5y9osyymQ" width="480" height="360" frameBorder="0" allowFullScreen></iframe>
   </div>
{% endblock %}
  1. {% extends "base.html" %} indica que la página usa base.html como plantilla.
  2. El bloque title pone un título propio en el <head>.
  3. El bloque content es lo que cambia entre páginas.

Reto: haz lo mismo con denied.html.

Bloques con super(). Como super().__init__() en las clases (si Simba hereda de Mufasa, Mufasa es el super), en las plantillas {{ super() }} trae el contenido del bloque del padre. En base.html:

HTML
<style>
{% block styling %}
body{
    background: purple;
}
{% endblock %}
</style>

Ambas páginas quedan con fondo morado. Para que en denied.html el <h1> sea además rojo, sin perder el morado:

HTML
{% block styling %}
   {{ super() }}
   h1 {
      color: red;
   }
{% endblock %}

📖 Explicación conceptual

Herencia de clases (Python) Herencia de plantillas (Jinja)
class Hijo(Padre): {% extends "padre.html" %}
Sobrescribir un método Redefinir un {% block %}
super().metodo() {{ super() }}
Métodos no redefinidos se heredan tal cual Bloques no redefinidos muestran el contenido del padre

Reglas importantes:

  • {% extends %} debe ser la primera instrucción de la plantilla hija.
  • En una plantilla hija, todo lo que esté fuera de un bloque se ignora.
  • Los nombres de los bloques deben coincidir exactamente con los del padre.

⚠️ Errores frecuentes

Error Causa Solución
El contenido de la página no aparece Está fuera de {% block content %} o el bloque tiene otro nombre Ponlo dentro del bloque con el nombre exacto
TemplateSyntaxError: ... 'endblock' Falta cerrar un bloque Cada {% block %} con su {% endblock %}
El estilo del padre desaparece en una página Redefiniste el bloque sin {{ super() }} Agrega {{ super() }} si quieres conservarlo

Retos adicionales (creados para esta adaptación)

  • 🟢 Reto 1: agrega a base.html un bloque footer con el año actual, y en denied.html agrégale un texto con {{ super() }}.
  • 🟡 Reto 2: convierte el blog del Día 59 de include a herencia: base.html con los bloques title, header_image, content.
  • 🟠 Reto 3: crea tres niveles: base.html → seccion.html (agrega una barra lateral) → pagina.html. ¿Funciona super() a través de los tres?

7. Bootstrap-Flask y render_form

(Fuentes: 438-using-bootstrap-flask-as-an-inherited-template.md y 439-bootstrap-flask-supports-wtforms.md)

🎯 Objetivo de aprendizaje

Usar la extensión Bootstrap-Flask para cargar Bootstrap y generar formularios de WTForms con una línea.

📝 Contenido original del curso

El sitio "parece nacido en los 90". En lugar de pegar el enlace de Bootstrap a mano, se usa la extensión Bootstrap-Flask:

  1. pip install bootstrap-flask
  2. Reto: borra el bloque con super() de denied.html y, con la documentación de Bootstrap-Flask, convierte denied.html, success.html, login.html e index.html para que usen Bootstrap. Pistas: inicializa la extensión en main.py y carga el CSS de Bootstrap en base.html.

render_form. La gran ventaja de Bootstrap-Flask: genera todo el formulario (etiquetas, campos, botón, estilos y mensajes de error) con una línea. Se borra el <form> completo, se importa la macro y se usa:

HTML
{% from 'bootstrap5/form.html' import render_form %}
...
{{ render_form(form) }}

¿Por qué el curso te hizo construir el formulario a mano si existía este atajo? "Todo va bien mientras funciona. render_form es una caja negra, es magia. ¿Pero qué pasa si tu formulario se rompe? ¿Cómo depuras la magia? Por eso es importante entender cómo funcionan las cosas por dentro. Una vez que lo entiendes, puedes tomar todos los atajos".

📖 Explicación conceptual (verificado con Bootstrap-Flask 2.6.0)

Python
from flask_bootstrap import Bootstrap5      # el paquete es bootstrap-flask; el módulo, flask_bootstrap

app = Flask(__name__)
app.secret_key = "clave-de-prueba"
bootstrap = Bootstrap5(app)
HTML
<!-- base.html -->
<!doctype html>
<html lang="es">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>{% block title %}{% endblock %}</title>
    {{ bootstrap.load_css() }}
</head>
<body>
    {% block content %}{% endblock %}
    {{ bootstrap.load_js() }}
</body>
</html>
HTML
<!-- login.html -->
{% extends "base.html" %}
{% from 'bootstrap5/form.html' import render_form %}
{% block title %}Login{% endblock %}
{% block content %}
<div class="container">
    <h1>Login</h1>
    {{ render_form(form, novalidate=True) }}
</div>
{% endblock %}
  • bootstrap.load_css() generó el enlace a Bootstrap 5.3.8 (verificado).
  • render_form incluye el token CSRF automáticamente y muestra los errores de validación con el estilo de Bootstrap.
  • novalidate=True desactiva la validación del navegador (verificado: el <form> generado lleva novalidate).

🛠️ Nota técnica: existen dos paquetes con nombres parecidos. Flask-Bootstrap (antiguo, Bootstrap 3, abandonado) y Bootstrap-Flask (el del curso, Bootstrap 4 y 5). Ambos se importan como flask_bootstrap, así que si instalaste el equivocado, los errores son confusos. Revisa con python -m pip show bootstrap-flask. En Bootstrap-Flask 2.x se usa Bootstrap5(app) y la ruta de macros bootstrap5/form.html (con Bootstrap4 y bootstrap4/... para Bootstrap 4).

⚠️ Errores frecuentes

Error Causa Solución
TemplateNotFound: bootstrap5/form.html No inicializaste la extensión, o usas Bootstrap4 Bootstrap5(app) en main.py
UndefinedError: 'bootstrap' is undefined Igual Inicializa la extensión
render_form no hace nada Olvidaste el {% from ... import render_form %} Impórtalo en la plantilla donde lo usas
Estilo de Bootstrap 3 y errores extraños Instalaste Flask-Bootstrap en lugar de Bootstrap-Flask Desinstala el antiguo e instala bootstrap-flask

8. 🚀 Proyecto del día: la página de los secretos

(Integra las lecciones 432 a 439.)

Problema

Una página con información privada debe mostrarse solo a quien conozca el correo y la contraseña, con un formulario claro, validado y con buen aspecto.

Objetivo

Un sitio con portada, formulario de inicio de sesión con Flask-WTF y Bootstrap-Flask, y las páginas success.html y denied.html, todas heredando de base.html.

Requisitos funcionales

  1. LoginForm con email (DataRequired, Email), password (PasswordField, DataRequired, Length(min=8)) y submit.
  2. /login con GET y POST; validate_on_submit(); success.html o denied.html según las credenciales del curso.
  3. Todas las páginas heredan de base.html con Bootstrap-Flask.
  4. El formulario se muestra con render_form y novalidate.

Restricciones técnicas

  • Entorno virtual propio y requirements.txt actualizado (incluido email_validator).
  • Clave secreta leída de una variable de entorno, con un valor por defecto solo para desarrollo.

Planificación

  1. Comprender los archivos de inicio. 2. Entradas: correo y contraseña. 3. Procesos: validar → comparar → decidir la página. 4. Salidas: 3 páginas. 5. Dividir: clase del formulario, ruta, plantillas. 6. Diseñar base.html y sus bloques. 7. Implementar. 8. Probar. 9. Corregir. 10. Mejorar.

Construcción paso a paso

  • Versión 1: ruta /login que renderiza login.html (reto de la lección 432).
  • Versión 2: formulario con StringField y csrf_token.
  • Versión 3: mejoras de la lección 434.
  • Versión 4: validadores y mensajes de error.
  • Versión 5: success.html y denied.html.
  • Versión final: herencia de plantillas y Bootstrap-Flask con render_form.

Pruebas

Caso Resultado esperado (verificado)
GET /login Formulario con Bootstrap
Enviar vacío "This field is required." en ambos campos
adminemail.com y 123 "Invalid email address." y "Field must be at least 8 characters long."
admin@email.com y 12345678 success.html
admin@email.com y 87654321 denied.html
Sin email_validator instalado Error 500 al enviar (por eso debe estar en requirements.txt)

🧪 Prueba y depuración

Python
from flask_wtf import FlaskForm
from wtforms import StringField
from wtforms.validators import DataRequired, Email

class LoginForm(FlaskForm):
    email = StringField("Email", validators=DataRequired())
    password = StringField("Password", validators=[DataRequired])

@app.route("/login")
def login():
    form = LoginForm()
    if form.validate_on_submit:
        ...

Encuentra cinco problemas: falta app.secret_key; validators debe ser una lista; DataRequired va con paréntesis (una instancia, no la clase); la contraseña debería ser PasswordField; la ruta no acepta POST y validate_on_submit se usa sin paréntesis (siempre es "verdadero" porque es un método, no su resultado).

⭐ Extensión del proyecto

  • Tras tres intentos fallidos, mostrar "Demasiados intentos" (cuenta con una variable; el Día 68 verás sesiones).
  • Personalizar el tema de Bootstrap con app.config["BOOTSTRAP_BOOTSWATCH_THEME"] (documentación de Bootstrap-Flask).

9. 🌟 Proyecto extra: la inscripción a la salida pedagógica

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

Situación

El colegio organiza una salida pedagógica al museo y necesita inscribir a los estudiantes con autorización de sus acudientes.

Requisitos

  1. InscripcionForm con: nombre del estudiante, curso (SelectField de 6.° a 11.°), nombre del acudiente, correo del acudiente (Email), teléfono (validador Regexp para 10 dígitos), restricción alimentaria (TextAreaField, opcional, máximo 200 caracteres) y una casilla "Autorizo la salida" (BooleanField con DataRequired).
  2. base.html con Bootstrap-Flask; inscripcion.html y confirmacion.html heredan de ella.
  3. Al enviar un formulario válido, guardar la inscripción en inscripciones.csv con el módulo csv y encoding="utf-8", y mostrar la confirmación con los datos.
  4. Mensajes de error en español.

Decisiones que debes tomar (y justificar)

  • ¿Qué datos son realmente necesarios? ¿Por qué no pedir el documento de identidad?
  • ¿Quién puede ver inscripciones.csv? ¿Cuándo se borra?

Casos de prueba

  • Casilla de autorización sin marcar → error; nada se guarda.
  • Teléfono 300123 → error de formato.
  • Inscripción válida con tildes ("Martínez") → se guarda y se lee bien.

10. 🏗️ Proyecto integrador del bloque avanzado — Etapa 3

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

La Plataforma del Club — Etapa 3 (hoy):

  1. Entorno virtual propio y requirements.txt.
  2. Reemplaza include por herencia: base.html con Bootstrap-Flask y bloques title y content.
  3. Convierte los formularios de contacto e inscripción del Día 60 a Flask-WTF con validadores y render_form.
  4. Clave secreta en .env.

11. 🧩 Comprueba lo aprendido

Conceptuales

  1. Nombra tres ventajas de Flask-WTF sobre un formulario HTML simple.
  2. ¿Qué es un ataque CSRF y cómo lo evita el token?
  3. ¿Para qué sirve app.secret_key y por qué no debe publicarse?
  4. ¿Qué devuelve validate_on_submit() en un GET? ¿Y en un POST con errores?
  5. ¿En qué se parecen {{ super() }} y super().__init__()?

Análisis de código

HTML
{% extends "base.html" %}
<p>Hola</p>
{% block content %}<h1>Página</h1>{% endblock %}

¿Se ve el párrafo "Hola"? ¿Por qué?

Predicción

Con Length(min=8) y la contraseña "abc 1234", ¿se valida? ¿Y con " " (8 espacios) y DataRequired()?

Depuración

Al enviar el formulario, la página se recarga sin mostrar errores ni avanzar. Nombra dos causas posibles (pista: revisa la plantilla y la configuración).

Modificación

Cambia el proyecto para que denied.html muestre el correo que se intentó usar (form.email.data).

Creación

Crea un formulario de encuesta con RadioField ("¿Cómo calificas la cafetería?", de 1 a 5) y un comentario opcional; muestra el resultado en una página que herede de base.html.


12. 🤖 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 — ¿Qué paquete? Pide a una IA el código para usar Bootstrap con Flask. ¿Menciona Flask-Bootstrap o Bootstrap-Flask? ¿Bootstrap(app) o Bootstrap5(app)? Compáralo con la documentación de Bootstrap-Flask y con pip show.

Actividad 2 — El error 500 del correo. Muestra a una IA el mensaje Install 'email_validator'. Verifica su solución y explica por qué el error aparece al enviar y no al cargar la página.

Actividad 3 — Explicar la magia. Genera un formulario con render_form y mira el HTML resultante (Ctrl + U). Pide a una IA que te explique cada parte (el input oculto, las clases form-control, invalid-feedback). Comprueba cada explicación en la documentación.


13. 🛠️ Correcciones técnicas al material original

Archivo Afirmación o comportamiento del curso Observación Tratamiento
435 Pista: "fíjate si hay que instalar un paquete adicional" Sin email_validator, Email() produce un error 500 al enviar (verificado) Nota técnica de la sección 4
433 app.secret_key = "some secret string" Válido en ejercicios; en proyectos reales va en .env y se genera al azar Sección 2: la clave secreta
436 Credenciales admin@email.com / 12345678 en el código Aceptable para practicar; no para aplicaciones reales (Día 68) Nota técnica de la sección 5
438 pip install bootstrap-flask e inicializarlo según la documentación Existe el paquete antiguo Flask-Bootstrap con el mismo módulo flask_bootstrap; en la versión 2.x se usa Bootstrap5 Nota técnica de la sección 7
434, 435, 436, 438, 439 Código mostrado en imágenes Reconstruido con la documentación oficial y verificado Secciones 3 a 7
432, 433, 435 Enlaces a la documentación de WTForms 3.0 y Flask-WTF 1.0 El código funciona con WTForms 3.2.2 y Flask-WTF 1.3.0 (verificado) —

14. 📋 Matriz de trazabilidad

Contenido original Archivo de origen Concepto desarrollado Ejercicio original Reto adicional Proyecto relacionado
Ventajas de Flask-WTF (validación, menos código, CSRF), página de secretos 431 Sección 0 — CSRF con un ejemplo Sección 8
Archivos de inicio, entorno virtual en PyCharm, requirements.txt, pip install, reto de la ruta /login 432 Sección 1 Ruta /login Entorno virtual desde la terminal Secciones 8 y 10
Quickstart de Flask-WTF, StringField, tamaño 30, csrf_token, secret_key, repaso de clases 433 Sección 2 Formulario de inicio de sesión La clave secreta Sección 8
PasswordField, label=, action dinámico, maquetación, SubmitField 434 Sección 3 Mejoras del código — Sección 8
Validadores, DataRequired, form.campo.errors, validate_on_submit, validación del navegador, novalidate, reto de Email y Length 435 Sección 4 Reto de validación Retos 1–4 de la sección 4 Secciones 8 y 9
.data, validate_on_submit() en lugar de request.method, reto success.html / denied.html 436 Sección 5 Reto de credenciales Tabla de los cuatro caminos Sección 8
Herencia de plantillas, base.html, extends, bloques, reto de denied.html, super(), bloque styling 437 Sección 6 Retos de herencia Retos 1–3 de la sección 6 Secciones 8, 9 y 10
Bootstrap-Flask, pip install bootstrap-flask, reto de convertir las 4 páginas 438 Sección 7 Reto de Bootstrap-Flask Paquetes con nombres parecidos Secciones 8 y 10
render_form, importar la macro, "¿cómo depuras la magia?", proyecto final 439 Sección 7 render_form — Secciones 8, 9 y 10
(No existe en el curso original) — — — Inscripción a la salida pedagógica Sección 9
(No existe en el curso original) — — — Plataforma del Club, etapa 3 Sección 10

15. 📦 Evidencias de aprendizaje del Día 61

  • La página de los secretos funcionando, con capturas de los errores de validación, success.html y denied.html.
  • Tu requirements.txt y el comando con el que creaste el entorno virtual.
  • El HTML generado por render_form (Ctrl + U) con tres partes explicadas.
  • La inscripción a la salida pedagógica (proyecto extra).
  • La etapa 3 de la Plataforma del Club.
  • Cuaderno de errores: módulo no encontrado por instalar en otro Python, falta de secret_key, falta de csrf_token, validators sin lista, DataRequired sin paréntesis, validate_on_submit sin paréntesis, email_validator no instalado, contenido fuera de un bloque, Flask-Bootstrap contra Bootstrap-Flask.