Saltar al contenido
Volver al inicio 100 Días de Código en Python — Día 69: Usuarios, administrador, relaciones y comentarios (proyecto del blog, parte 4)

100 Días de Código en Python — Día 69: Usuarios, administrador, relaciones y comentarios (proyecto del blog, parte 4)

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

Este tutorial adapta pedagógicamente el Día 69, el último paso del proyecto del blog. Las secciones 📝 Ejercicio original del curso provienen de las lecciones 494 a 500; las soluciones oficiales están en los gists enlazados y los archivos de inicio y final (day-69-starting-files-blog-with-users.zip, con blog.db, y day-69-blog-with-users-end.zip) en los recursos. Los retos 🟢🟡🟠🔴, las secciones sobre autorización segura, comentarios seguros y privacidad de Gravatar, el proyecto extra y la etapa 11 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-Login 0.6.3, Flask-SQLAlchemy 3.1.1 y SQLAlchemy 2.1.1, se construyeron los modelos User, BlogPost y Comment con relaciones uno-a-muchos bidireccionales y se probaron en ambas direcciones. Se comprobó que un decorador @admin_only que solo compara current_user.id != 1 produce un error 500 con un visitante anónimo (AttributeError: 'AnonymousUserMixin' object has no attribute 'id'), y que Gravatar responde con imágenes tanto para hashes MD5 como SHA-256 del correo.


Tabla de contenidos

  1. Antes de empezar: un blog con lectores
  2. Requisito 1: registrar usuarios
  3. Requisito 2: iniciar sesión, mensajes y barra de navegación
  4. Requisito 3: proteger rutas con @admin_only
  5. Bases de datos relacionales: usuarios y entradas
  6. Requisito 4: comentarios
  7. 🔒 Comentarios seguros
  8. Avatares con Gravatar
  9. 🚀 Proyecto del día: el blog completo con usuarios
  10. 🌟 Proyecto extra: el foro de preguntas de la clase
  11. 🏗️ Proyecto integrador del bloque avanzado — Etapa 11
  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 69

0. Antes de empezar: un blog con lectores

(Fuentes: 494-day-69-goals-adding-users-to-our-blog-project.md y 495-download-the-starting-project.md)

📝 Contenido original del curso

¿Y si cualquiera pudiera registrarse y comentar las entradas? Para eso hay que agregar autenticación al blog. Es el paso final del proyecto: al terminar tendrás un blog completo que puedes publicar.

El proyecto de inicio (entorno virtual, requirements.txt) trae blog.db con entradas de ejemplo. Recórrelo con DB Browser y en el navegador: las páginas de registro e inicio de sesión están incompletas. "Revisa todo el código antes de empezar".

Prerequisitos: el blog con CRUD (Día 67), autenticación con Flask-Login y Werkzeug (Día 68), decoradores con *args/**kwargs y functools.wraps (Días 54 y 55), Flask-WTF, Bootstrap-Flask y CKEditor (Días 61 y 67).

📖 Roles en el blog (adaptación)

Rol Puede
Visitante (sin sesión) Leer entradas y comentarios
Lector registrado Todo lo anterior + comentar
Administrador (el usuario con id 1) Todo lo anterior + crear, editar y borrar entradas

Esto es autorización: decidir qué puede hacer cada quien (Día 68).


1. Requisito 1: registrar usuarios

(Fuente: 496-requirement-1-register-new-users.md)

📝 Ejercicio original del curso

  1. Crea en forms.py un formulario de WTForms RegisterForm.
  2. Crea una tabla User en la base de datos; los datos del formulario crean un registro en ella.
  3. En la ruta /register, crea el usuario con la contraseña con hash y sal usando Werkzeug.
  4. Renderiza el formulario en register.html con render_form() de Bootstrap-Flask.

Pista: todavía sin Flask-Login; solo crear el usuario.

📖 Explicación conceptual

Python
class RegisterForm(FlaskForm):
    email = StringField("Email", validators=[DataRequired(), Email()])
    password = PasswordField("Password", validators=[DataRequired(), Length(min=12)])
    name = StringField("Name", validators=[DataRequired()])
    submit = SubmitField("Sign Me Up!")

La ruta es la misma del Día 68, ahora con WTForms: form.validate_on_submit(), generate_password_hash(form.password.data) y db.session.add(...).

🛠️ Nota técnica: la lección dice "una tabla User dentro de posts.db", pero el proyecto de este día usa blog.db. Es la misma idea: la tabla nueva va en la base de datos del proyecto. Recuerda email_validator si usas Email() (Día 61).


2. Requisito 2: iniciar sesión, mensajes y barra de navegación

(Fuente: 497-requirement-2-login-registered-users.md)

📝 Ejercicio original del curso

  1. Los usuarios registrados inician sesión en /login (Flask-Login y lo del día anterior). Pistas: buscar al usuario con where() por su correo; render_form en login.html.
  2. Una línea en /register para que, al registrarse, el usuario quede con la sesión iniciada y vuelva a la portada.
  3. Si alguien se registra con un correo que ya existe: redirigir a /login con un mensaje flash (<p class="flash">) que le pida iniciar sesión.
  4. En /login, si el correo no existe o la contraseña no coincide: volver a /login con un mensaje que explique el problema.
  5. La barra de navegación (header.html) muestra "Login" y "Register" a quien no ha iniciado sesión, y "Log Out" a quien sí.
  6. La ruta /logout cierra la sesión y vuelve a la portada.

📖 Explicación conceptual

Es exactamente el Día 68 aplicado al blog. Lo nuevo es la barra de navegación compartida en header.html (Día 59): como se incluye en todas las páginas, basta con un {% if current_user.is_authenticated %} para que todo el sitio se adapte.


3. Requisito 3: proteger rutas con @admin_only

(Fuente: 498-requirement-3-protect-routes.md)

🎯 Objetivo de aprendizaje

Escribir un decorador propio de autorización y responder 403 a quien no tiene permiso.

📝 Ejercicio original del curso

En el blog, el primer usuario registrado es el administrador: solo él crea, edita y borra entradas.

  1. El primer usuario tiene id 1. Úsalo en index.html y post.html para que solo el administrador vea los botones "Create New Post", "Edit Post" y borrar.
  2. Que no vea los botones no impide que alguien escriba /edit-post, /new-post o /delete a mano. Protege esas rutas con un decorador propio @admin_only: si el id de current_user es 1, puede entrar; si no, recibe un error 403.

Pistas: repasa los decoradores (Día 54); mira cómo está hecho el decorador login_required de Flask; usa la función abort() para devolver errores HTTP.

📖 Explicación conceptual (verificada)

La versión que compara solo el id falla con los visitantes anónimos:

Python
def admin_only(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
        if current_user.id != 1:          # ⚠️ un visitante anónimo no tiene .id
            return abort(403)
        return f(*args, **kwargs)
    return decorated_function

Verificado: un visitante sin sesión que entra a /new-post provoca AttributeError: 'AnonymousUserMixin' object has no attribute 'id' y un error 500. La corrección:

Python
from functools import wraps
from flask import abort


def admin_only(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
        if not current_user.is_authenticated or current_user.id != 1:
            return abort(403)
        return f(*args, **kwargs)
    return decorated_function


@app.route("/new-post", methods=["GET", "POST"])
@admin_only
def add_new_post():
    ...

Resultados verificados con la versión corregida: anónimo → 403; lector → 403; administrador → 200.

Por qué ocultar el botón no basta: la plantilla decide qué se ve; el decorador decide qué se puede hacer. La seguridad siempre se aplica en el servidor.

🛠️ Nota técnica: "el administrador es el usuario con id 1" funciona para aprender, pero en un sitio publicado el primero que se registre se vuelve administrador (¡podría ser un desconocido!). Lo robusto es una columna es_admin (o rol) en User, que se asigna a mano o con un script, y el decorador comprueba current_user.es_admin.

Retos adicionales (creados para esta adaptación)

  • 🟢 Reto 1 — Aplicación: personaliza la página de error 403 con @app.errorhandler(403) y el diseño del blog.
  • 🟡 Reto 2 — Ampliación: reemplaza id == 1 por una columna es_admin y crea un script hacer_admin.py <correo>.
  • 🟠 Reto 3 — Desafío: escribe un decorador con parámetro, @rol_requerido("editor"), para un blog con roles lector, editor y administrador (pista: tres niveles de funciones, como @app.route("/"), Día 55).
  • 🔴 Reto 4 — Pensamiento computacional: permite que un editor edite solo sus propias entradas y el administrador, todas. ¿Dónde va esa comprobación: en el decorador o dentro de la ruta? ¿Por qué?

4. Bases de datos relacionales: usuarios y entradas

(Fuente: 499-creating-relational-databases.md)

🎯 Objetivo de aprendizaje

Crear una relación uno-a-muchos bidireccional con ForeignKey y relationship(), y entender por qué un cambio de esquema rompe la base de datos existente.

📝 Contenido original del curso

Como el primer usuario es el dueño del blog, tiene sentido vincular cada entrada con el usuario que la escribió (y, en el futuro, invitar a otros autores). Hace falta una relación entre las tablas User y BlogPost: ver qué entradas escribió un usuario, o qué usuario escribió una entrada.

Con Python puro, un User podría tener un atributo posts con una lista de BlogPost. Así es fácil ir del usuario a sus entradas… pero ¿cómo ir de una entrada a su autor? Para eso están las bases de datos relacionales (SQLite, MySQL, PostgreSQL), con ForeignKey (clave foránea) y relationship().

Reto 1: modifica User y BlogPost para crear una relación uno-a-muchos bidireccional: User es el padre y BlogPost el hijo. Esto agrega una clave foránea author_id: es un cambio que rompe el sitio; verás OperationalError: no such column: blog_posts.author_id.

Recrear la base de datos: la base vieja no tiene la columna author_id. Como no hay que conservar los datos de prueba, detén el servidor, borra blog.db, reinicia, registra un nuevo administrador y crea una entrada. "Esto muestra algo importante: el esquema de la base de datos debe definirse temprano. Con una aplicación publicada y llena de datos, habría que migrar la base de datos para conservarlos".

Reto 2: el nombre del autor desapareció de index.html y post.html. Corrígelo. Pista: author ahora es un objeto User.

📖 Explicación conceptual (verificada con SQLAlchemy 2.1)

Python
from typing import List
from sqlalchemy import ForeignKey
from sqlalchemy.orm import relationship


class User(UserMixin, db.Model):
    __tablename__ = "users"
    id: Mapped[int] = mapped_column(Integer, primary_key=True)
    ...
    posts: Mapped[List["BlogPost"]] = relationship(back_populates="author")


class BlogPost(db.Model):
    __tablename__ = "blog_posts"
    id: Mapped[int] = mapped_column(Integer, primary_key=True)
    author_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
    author: Mapped["User"] = relationship(back_populates="posts")
    ...
Pieza Qué es
author_id La columna real en la tabla blog_posts: guarda el id del usuario
ForeignKey("users.id") Dice que ese número apunta a la columna id de la tabla users
author Un atributo de Python (no es columna) que entrega el objeto User
posts La lista de entradas del usuario
back_populates Conecta las dos direcciones: si agregas una entrada a usuario.posts, su author se actualiza

Verificado: con una entrada creada como BlogPost(title="Cactus", author=admin), entrada.author.name da "Admin" y admin.posts da [entrada].

En las plantillas: {{ post.author }} ahora muestra algo como <User 1>; lo correcto es {{ post.author.name }} (Reto 2 del curso).

📖 Migraciones (adaptación)

Borrar blog.db está bien con datos de prueba. Con datos reales se usa una herramienta de migraciones, como Flask-Migrate (basada en Alembic), que compara tus modelos con la base de datos y genera los cambios (ALTER TABLE ... ADD COLUMN ...) sin perder información. Vale la pena conocerla antes de publicar un proyecto.

⚠️ Errores frecuentes

Error Causa Solución
OperationalError: no such column: blog_posts.author_id La base de datos es de antes del cambio Borrar y recrear (en desarrollo) o migrar
NoForeignKeysError o errores de relationship ForeignKey apunta a un nombre de tabla equivocado ("user.id" en lugar de "users.id") Usa el __tablename__ exacto
La página muestra <User 1> Mostraste el objeto, no su nombre post.author.name
back_populates que no coinciden Los nombres de los dos lados no se corresponden posts ↔ author en ambos lados

🛠️ Nota técnica: el ejemplo de Python puro de la lección tiene dos errores: User(...) recibe posts=[...], pero __init__ no tiene ese parámetro (sería TypeError), y termina con } en lugar de ) (sería SyntaxError). La idea (un usuario con una lista de entradas) es correcta.


5. Requisito 4: comentarios

(Fuente: 500-requirement-4-allow-any-user-to-add-comments-to-blogposts.md, pasos 1 a 7)

📝 Ejercicio original del curso

  1. Crea en forms.py un CommentForm con un solo CKEditorField.
  2. Crea la tabla Comment (__tablename__ = "comments") con id (clave primaria) y text.
  3. Relación uno-a-muchos entre User (padre) y Comment (hijo): un usuario, muchos comentarios.
  4. Relación uno-a-muchos entre BlogPost (padre) y Comment (hijo): una entrada, muchos comentarios.
  5. Cambiaste el esquema otra vez: detén el servidor, borra blog.db, reinicia, registra el administrador, crea una entrada y crea otro usuario, un lector.
  6. Solo los usuarios autenticados pueden comentar; si no, mensaje flash pidiendo iniciar sesión y redirección a /login. Actualiza la ruta /post/<int:post_id> (pista: ¿necesita POST?). Prueba como el lector "John Doe" y revisa el comentario en blog.db.
  7. Muestra en post.html todos los comentarios de la entrada. Pistas: comments es un atributo de cada entrada y se usa como lista; el texto viene de CKEditor, así que está en HTML.

📖 Explicación conceptual

Python
class Comment(db.Model):
    __tablename__ = "comments"
    id: Mapped[int] = mapped_column(Integer, primary_key=True)
    text: Mapped[str] = mapped_column(Text, nullable=False)
    author_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
    comment_author: Mapped["User"] = relationship(back_populates="comments")
    post_id: Mapped[int] = mapped_column(ForeignKey("blog_posts.id"))
    parent_post: Mapped["BlogPost"] = relationship(back_populates="comments")

# y en User:      comments: Mapped[List["Comment"]] = relationship(back_populates="comment_author")
# y en BlogPost:  comments: Mapped[List["Comment"]] = relationship(back_populates="parent_post")


@app.route("/post/<int:post_id>", methods=["GET", "POST"])
def show_post(post_id):
    requested_post = db.get_or_404(BlogPost, post_id)
    comment_form = CommentForm()
    if comment_form.validate_on_submit():
        if not current_user.is_authenticated:
            flash("You need to login or register to comment.")
            return redirect(url_for("login"))
        new_comment = Comment(text=comment_form.comment_text.data,
                              comment_author=current_user, parent_post=requested_post)
        db.session.add(new_comment)
        db.session.commit()
        return redirect(url_for("show_post", post_id=post_id))   # POST/Redirect/GET (Día 60)
    return render_template("post.html", post=requested_post, current_user=current_user, form=comment_form)
HTML
{% for comment in post.comments %}
  <li>
    <div class="commentText">{{ comment.text|safe }}</div>
    <span class="sub-text">{{ comment.comment_author.name }}</span>
  </li>
{% endfor %}

Comment tiene dos claves foráneas: pertenece a un usuario y a una entrada. Verificado: entrada.comments[0].comment_author.name da el nombre del lector, y lector.comments[0].parent_post.title da el título de la entrada.


6. 🔒 Comentarios seguros

(Sección creada para esta adaptación a partir del paso 7 de la lección 500.)

En el Día 67 el curso aceptó mostrar el cuerpo de las entradas con |safe "porque confiamos en nuestros autores". Hoy, cualquier persona que se registre puede escribir comentarios, y se muestran con |safe. Un comentario con <script> (enviado sin pasar por CKEditor, por ejemplo con requests.post) se ejecutaría en el navegador de todos los que lean la entrada, incluido el administrador: podría, por ejemplo, hacer acciones en su nombre. Es un XSS almacenado, uno de los ataques más serios.

Solución obligatoria en este proyecto: limpiar el HTML con nh3 (Día 67) antes de guardarlo, y permitir solo unas pocas etiquetas:

Python
import nh3

ETIQUETAS_PERMITIDAS = {"p", "b", "strong", "i", "em", "u", "ul", "ol", "li", "a", "br"}

texto_limpio = nh3.clean(comment_form.comment_text.data, tags=ETIQUETAS_PERMITIDAS)

Otras medidas razonables: un límite de longitud (Length(max=2000)) y la posibilidad de que el administrador borre comentarios.


7. Avatares con Gravatar

(Fuente: 500-requirement-4-allow-any-user-to-add-comments-to-blogposts.md, sección final)

📝 Ejercicio original del curso

Gravatar ofrece imágenes de perfil para comentaristas en muchos blogs; cada persona sube su imagen en gravatar.com asociada a su correo. El paquete flask-gravatar no es compatible con las versiones recientes de Flask (no se actualiza desde 2018), pero no hace falta: basta con calcular el hash del correo y pedirle la imagen a Gravatar (ver la sección de avatares del Flask Mega-Tutorial y la documentación de imágenes de Gravatar).

📖 Explicación conceptual (verificada)

Python
import hashlib


def gravatar_url(email, size=100):
    digest = hashlib.sha256(email.strip().lower().encode("utf-8")).hexdigest()
    return f"https://www.gravatar.com/avatar/{digest}?d=identicon&s={size}"


app.jinja_env.globals["gravatar_url"] = gravatar_url   # disponible en todas las plantillas
HTML
<img src="{{ gravatar_url(comment.comment_author.email) }}" alt="Avatar de {{ comment.comment_author.name }}">
  • El correo se normaliza (strip().lower()) antes del hash: "Ana@Correo.co " y "ana@correo.co" son el mismo.
  • d=identicon genera un dibujo geométrico para quien no tenga cuenta en Gravatar.
  • Gravatar acepta el hash SHA-256 y el MD5 que usan tutoriales más antiguos (ambos verificados).

📖 Privacidad (adaptación)

Publicar el hash del correo no lo oculta del todo: como viste en el Día 68, un atacante puede calcular el hash de una lista de correos probables y compararlos. Para un proyecto escolar con menores de edad, considera generar el avatar a partir del id o del nombre del usuario (por ejemplo, con las iniciales y un color) en lugar de su correo.


8. 🚀 Proyecto del día: el blog completo con usuarios

(Integra los requisitos 1 a 4 del curso: 496 a 500.)

Problema

El blog necesita lectores que participen, un administrador que controle el contenido y datos conectados entre sí (quién escribió qué).

Objetivo

El blog final del curso: registro e inicio de sesión, administrador protegido con @admin_only, relaciones entre usuarios, entradas y comentarios, y avatares.

Requisitos funcionales

Los 4 requisitos del curso, más las correcciones de seguridad de las secciones 3 y 6.

Restricciones técnicas

  • @admin_only que responde 403 (no 500) a anónimos y lectores.
  • Comentarios limpiados con nh3.
  • Relaciones con back_populates en ambos lados.
  • SECRET_KEY en .env; contraseñas con Werkzeug.

Planificación

  1. Comprender blog.db y el código de inicio. 2. Entradas: formularios. 3. Procesos: registrar, autenticar, autorizar, relacionar, comentar. 4. Salidas: páginas según el rol. 5. Dividir en los 4 requisitos. 6. Diseñar el esquema en papel (tres tablas y sus flechas). 7. Implementar. 8. Probar con tres roles. 9. Corregir. 10. Mejorar.

Construcción paso a paso

  • Versión 1: registro.
  • Versión 2: inicio y cierre de sesión, mensajes y barra de navegación.
  • Versión 3: @admin_only y botones ocultos.
  • Versión 4: relación usuario-entradas y base recreada.
  • Versión 5: comentarios.
  • Versión final: limpieza de HTML y avatares.

Pruebas

Caso Anónimo Lector Administrador
Ver la portada y las entradas ✅ ✅ ✅
Ver botones de crear, editar, borrar ❌ ❌ ✅
Entrar a /new-post escribiendo la URL 403 403 200
Comentar Redirige a /login con mensaje ✅ ✅
Comentario con <script> — Se guarda sin el script —
Autor de una entrada Se ve el nombre, no <User 1> igual igual

🧪 Prueba y depuración

Python
def admin_only(f):
    def decorated_function():
        if current_user.id == 1:
            return f()
        return abort(403)
    return decorated_function

@admin_only
@app.route("/delete/<int:post_id>")
def delete_post(post_id):
    ...

Encuentra cuatro problemas: falta @wraps(f) (dos rutas con el mismo decorador chocarían, Día 55); el wrapper no acepta *args, **kwargs, así que falla con post_id; con un visitante anónimo, current_user.id da error 500; y @admin_only está encima de @app.route, así que Flask registra la función sin protección.

⭐ Extensión del proyecto

  • El administrador puede borrar comentarios; cada lector, solo los suyos.
  • Contador de comentarios en la portada ({{ post.comments|length }}).
  • Una página de perfil /usuario/<int:id> con sus comentarios.

9. 🌟 Proyecto extra: el foro de preguntas de la clase

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

Situación

Muchos estudiantes no preguntan en clase por pena. El docente quiere un foro donde publicar dudas sobre los temas y responderlas entre todos.

Requisitos

  1. Tres tablas relacionadas: Usuario (con rol: estudiante o docente), Pregunta (título, texto, tema, autor, fecha, resuelta sí/no) y Respuesta (texto, autor, pregunta, fecha, marcada como "mejor respuesta" sí/no).
  2. Registro e inicio de sesión; solo usuarios autenticados preguntan y responden.
  3. Solo el autor de la pregunta o un docente puede marcar la mejor respuesta y la pregunta como resuelta (decorador o comprobación en la ruta).
  4. Texto limpiado con nh3.
  5. Portada con las preguntas sin resolver primero y filtro por tema.

Decisiones que debes tomar (y justificar)

  • ¿Permitir preguntas anónimas para los demás estudiantes (pero no para el docente)? ¿Cómo lo guardas en la base de datos?
  • ¿Qué hacer con respuestas ofensivas?

Casos de prueba

  • Un estudiante que no es el autor intenta marcar la mejor respuesta → 403.
  • Al marcar la mejor respuesta, la pregunta pasa a "resuelta" y baja en la portada.
  • pregunta.respuestas y respuesta.pregunta funcionan en ambas direcciones.

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

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

La Plataforma del Club — Etapa 11 (hoy):

  1. Columna rol en Usuario (miembro o coordinador) y un decorador @solo_coordinador que responda 403 correctamente también a los anónimos.
  2. Relación uno-a-muchos Usuario → Noticia (autor) y una relación muchos-a-muchos Usuario ↔ Actividad para las inscripciones (investiga en la documentación de SQLAlchemy la "tabla de asociación").
  3. Comentarios en las noticias, limpiados con nh3.
  4. Página "Mis actividades" con las actividades en las que el usuario está inscrito.

11. 🧩 Comprueba lo aprendido

Conceptuales

  1. ¿Qué es una clave foránea? ¿Qué diferencia hay entre author_id y author?
  2. ¿Qué significa que una relación sea "bidireccional"?
  3. ¿Por qué hubo que borrar blog.db al agregar author_id? ¿Qué se haría con datos reales?
  4. ¿Por qué ocultar un botón no protege una ruta?
  5. ¿Por qué los comentarios con |safe son más peligrosos que las entradas del administrador?

Análisis de código

Python
entrada = db.get_or_404(BlogPost, 1)
print(entrada.author.name, len(entrada.comments))
print([c.comment_author.name for c in entrada.comments])

¿Qué información obtiene cada línea y de qué tablas viene?

Predicción

Con el @admin_only original del curso (solo current_user.id != 1), ¿qué código de estado recibe un visitante sin sesión en /new-post? ¿Y con la versión corregida?

Depuración

"Después de agregar la tabla Comment, al abrir cualquier entrada sale no such table: comments". ¿Qué pasó y cómo se resuelve en desarrollo?

Modificación

Cambia @admin_only para que, en lugar de 403, redirija a la portada con un mensaje flash. ¿Qué opción es mejor para el usuario? ¿Y para un programa que use la API?

Creación

Escribe una ruta /autor/<int:user_id> que muestre el nombre del usuario, sus entradas y cuántos comentarios ha escrito, usando solo las relaciones (sin consultas extra).


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 — El decorador de administrador. Pide a una IA un decorador admin_only para Flask-Login. Pruébalo con un visitante anónimo. ¿Da 403 o 500? Compara con la sección 3.

Actividad 2 — Relaciones con el estilo actual. Pide a una IA los modelos User y BlogPost con una relación uno-a-muchos. ¿Usa db.Column y backref (estilo antiguo) o Mapped, mapped_column y back_populates (SQLAlchemy 2)? Compara con la documentación "Basic Relationship Patterns".

Actividad 3 — XSS en comentarios. Pregunta a una IA si es seguro mostrar comentarios con |safe. Pídele un ejemplo de ataque (solo como texto) y verifica que tu limpieza con nh3 lo neutraliza.


13. 🛠️ Correcciones técnicas al material original

Archivo Afirmación o comportamiento del curso Observación Tratamiento
498 @admin_only: "si el id del usuario es 1, puede entrar; si no, 403" Si se compara solo current_user.id, un visitante anónimo provoca un error 500 (verificado) Sección 3
498 El primer usuario registrado es el administrador En un sitio publicado, cualquiera podría registrarse primero Nota técnica de la sección 3
500 Mostrar los comentarios de cualquier usuario con |safe XSS almacenado; limpiar con nh3 Sección 6
499 Ejemplo de Python puro con posts=[...] TypeError (el constructor no tiene ese parámetro) y } en lugar de ) Nota técnica de la sección 4
496 "Tabla User dentro de posts.db" El proyecto de este día usa blog.db Nota técnica de la sección 1
499, 500 Borrar y recrear la base de datos Válido en desarrollo; con datos reales se usan migraciones (Flask-Migrate) Sección 4
500 Gravatar con el hash del correo Funciona con SHA-256 y MD5 (verificado); el hash no oculta del todo el correo Sección 7

14. 📋 Matriz de trazabilidad

Contenido original Archivo de origen Concepto desarrollado Ejercicio original Reto adicional Proyecto relacionado
Usuarios que comentan, autenticación, paso final del blog 494 Sección 0 — Roles Sección 8
Proyecto de inicio, blog.db, recorrer el código 495 Sección 0 Preparar el proyecto — Sección 8
RegisterForm, tabla User, hash con Werkzeug, render_form 496 Sección 1 Requisito 1 — Secciones 8 y 9
Inicio de sesión, sesión al registrarse, flash por correo repetido y credenciales erróneas, barra de navegación, /logout 497 Sección 2 Requisito 2 (pasos 1–6) — Secciones 8 y 9
Administrador id 1, botones ocultos, decorador @admin_only, 403, abort() 498 Sección 3 Requisito 3 Retos 1–4 de la sección 3 Secciones 8, 9 y 10
Relación usuario-entradas, ForeignKey, relationship(), uno-a-muchos bidireccional, author_id, error de columna, recrear la base, migración, post.author.name 499 Sección 4 Retos 1 y 2 Migraciones Secciones 8, 9 y 10
CommentForm, tabla comments, relaciones con User y BlogPost, recrear la base, comentar solo autenticado, mostrar comentarios, Gravatar, flask-gravatar incompatible, Mega-Tutorial 500 Secciones 5 y 7 Requisito 4 (pasos 1–7) y avatares Comentarios seguros; privacidad Secciones 8, 9 y 10
(No existe en el curso original) — Sección 6 — Limpieza de comentarios con nh3 Secciones 8, 9 y 10
(No existe en el curso original) — — — Foro de preguntas de la clase Sección 9
(No existe en el curso original) — — — Plataforma del Club, etapa 11 Sección 10

15. 📦 Evidencias de aprendizaje del Día 69

  • El blog completo, con la tabla de pruebas por rol y capturas de un comentario con avatar.
  • El diagrama del esquema (tres tablas y sus relaciones).
  • La prueba de que un anónimo recibe 403 (no 500) y de que un comentario con <script> se limpia.
  • El foro de preguntas de la clase (proyecto extra).
  • La etapa 11 de la Plataforma del Club.
  • Cuaderno de errores: no such column tras cambiar el esquema, ForeignKey con el nombre de tabla equivocado, <User 1> en la plantilla, back_populates mal emparejado, @admin_only con anónimos, decorador sin wraps ni *args, decorador encima de la ruta, |safe en comentarios.