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 paqueteemail_validatorprovoca un error 500; los mensajes de error deDataRequired,Length; que sin el token CSRF el formulario no se valida; la herencia con{{ super() }}; y quebootstrap.load_css()de Bootstrap-Flask carga Bootstrap 5.3.8.
Tabla de contenidos
- Antes de empezar: ¿por qué otra forma de hacer formularios?
- Instalar Flask-WTF: entornos virtuales y
requirements.txt - Crear formularios con clases
- Mejoras al código del formulario
- Validación
- Recibir los datos del formulario
- Herencia de plantillas con Jinja
- Bootstrap-Flask y
render_form - 🚀 Proyecto del día: la página de los secretos
- 🌟 Proyecto extra: la inscripción a la salida pedagógica
- 🏗️ Proyecto integrador del bloque avanzado — Etapa 3
- 🧩 Comprueba lo aprendido
- 🤖 Programar con apoyo de IA
- 🛠️ Correcciones técnicas al material original
- 📋 Matriz de trazabilidad
- 📦 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
- Descarga y descomprime los archivos de inicio.
- Á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. - 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
venvcon 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:
python -m pip install -r requirements.txt # Windows
pip3 install -r requirements.txt # macOSUn 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)
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
venvno se comparte ni se sube a GitHub (lo verás en el Día 70 con.gitignore); se comparterequirements.txt. python -m pipasegura 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
emailypassword; - 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
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)<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.labelgenera el<label>yform.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:
- Usar
PasswordFieldpara la contraseña: oculta lo que se escribe. - Escribir el argumento
label=explícitamente cuando no es obvio para qué sirve. - Usar una URL dinámica en el
action(url_for) en lugar de"/"fijo. - Ordenar etiquetas y campos con elementos HTML normales (por ejemplo, un
<p>por campo). - Usar
SubmitFieldpara el botón.
from wtforms import StringField, PasswordField, SubmitField
class LoginForm(FlaskForm):
email = StringField(label="Email")
password = PasswordField(label="Password")
submit = SubmitField(label="Log In")<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.
- Los validadores se pasan al crear cada campo, en una lista:
email = StringField(label="Email", validators=[DataRequired()])DataRequired obliga a escribir algo. Si hay errores, WTForms genera una lista de errores en form.<campo>.errors.
- Se recorren para mostrarlos:
{{ form.email(size=30) }}
{% for err in form.email.errors %}
<span style="color:red">{{ err }}</span>
{% endfor %}- La ruta debe aceptar POST y llamar a
validate_on_submit():
@app.route("/login", methods=["GET", "POST"])
def login():
login_form = LoginForm()
login_form.validate_on_submit()
return render_template("login.html", form=login_form)- 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
novalidateen 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
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 paqueteemail_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 arequirements.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 lanceValidationError) 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
@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.
<!-- base.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{% block title %}{% endblock %}</title>
</head>
<body>
{% block content %}{% endblock %}
</body>
</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 %}{% extends "base.html" %}indica que la página usabase.htmlcomo plantilla.- El bloque
titlepone un título propio en el<head>. - El bloque
contentes 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:
<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:
{% 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.htmlun bloquefootercon el año actual, y endenied.htmlagrégale un texto con{{ super() }}. - 🟡 Reto 2: convierte el blog del Día 59 de
includea herencia:base.htmlcon los bloquestitle,header_image,content. - 🟠 Reto 3: crea tres niveles:
base.html→seccion.html(agrega una barra lateral) →pagina.html. ¿Funcionasuper()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:
pip install bootstrap-flask- Reto: borra el bloque con
super()dedenied.htmly, con la documentación de Bootstrap-Flask, conviertedenied.html,success.html,login.htmleindex.htmlpara que usen Bootstrap. Pistas: inicializa la extensión enmain.pyy carga el CSS de Bootstrap enbase.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:
{% 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)
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)<!-- 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><!-- 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_formincluye el token CSRF automáticamente y muestra los errores de validación con el estilo de Bootstrap.novalidate=Truedesactiva la validación del navegador (verificado: el<form>generado llevanovalidate).
🛠️ Nota técnica: existen dos paquetes con nombres parecidos.
Flask-Bootstrap(antiguo, Bootstrap 3, abandonado) yBootstrap-Flask(el del curso, Bootstrap 4 y 5). Ambos se importan comoflask_bootstrap, así que si instalaste el equivocado, los errores son confusos. Revisa conpython -m pip show bootstrap-flask. En Bootstrap-Flask 2.x se usaBootstrap5(app)y la ruta de macrosbootstrap5/form.html(conBootstrap4ybootstrap4/...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
LoginFormconemail(DataRequired,Email),password(PasswordField,DataRequired,Length(min=8)) ysubmit./logincon GET y POST;validate_on_submit();success.htmlodenied.htmlsegún las credenciales del curso.- Todas las páginas heredan de
base.htmlcon Bootstrap-Flask. - El formulario se muestra con
render_formynovalidate.
Restricciones técnicas
- Entorno virtual propio y
requirements.txtactualizado (incluidoemail_validator). - Clave secreta leída de una variable de entorno, con un valor por defecto solo para desarrollo.
Planificación
- 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.htmly sus bloques. 7. Implementar. 8. Probar. 9. Corregir. 10. Mejorar.
Construcción paso a paso
- Versión 1: ruta
/loginque renderizalogin.html(reto de la lección 432). - Versión 2: formulario con
StringFieldycsrf_token. - Versión 3: mejoras de la lección 434.
- Versión 4: validadores y mensajes de error.
- Versión 5:
success.htmlydenied.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
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
InscripcionFormcon: nombre del estudiante, curso (SelectFieldde 6.° a 11.°), nombre del acudiente, correo del acudiente (Email), teléfono (validadorRegexppara 10 dígitos), restricción alimentaria (TextAreaField, opcional, máximo 200 caracteres) y una casilla "Autorizo la salida" (BooleanFieldconDataRequired).base.htmlcon Bootstrap-Flask;inscripcion.htmlyconfirmacion.htmlheredan de ella.- Al enviar un formulario válido, guardar la inscripción en
inscripciones.csvcon el módulocsvyencoding="utf-8", y mostrar la confirmación con los datos. - 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):
- Entorno virtual propio y
requirements.txt. - Reemplaza
includepor herencia:base.htmlcon Bootstrap-Flask y bloquestitleycontent. - Convierte los formularios de contacto e inscripción del Día 60 a Flask-WTF con validadores y
render_form. - Clave secreta en
.env.
11. 🧩 Comprueba lo aprendido
Conceptuales
- Nombra tres ventajas de Flask-WTF sobre un formulario HTML simple.
- ¿Qué es un ataque CSRF y cómo lo evita el token?
- ¿Para qué sirve
app.secret_keyy por qué no debe publicarse? - ¿Qué devuelve
validate_on_submit()en un GET? ¿Y en un POST con errores? - ¿En qué se parecen
{{ super() }}ysuper().__init__()?
Análisis de código
{% 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.htmlydenied.html. - Tu
requirements.txty 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 decsrf_token,validatorssin lista,DataRequiredsin paréntesis,validate_on_submitsin paréntesis,email_validatorno instalado, contenido fuera de un bloque,Flask-BootstrapcontraBootstrap-Flask.