Banco de 20 Retos de Live Coding — Unidad 3
Los 20 retos parten de tu propio proyecto Django tal como queda al terminar la práctica de la Semana 6: el modelo Servicio con propietario, el ServicioForm con clean_precio, el CRUD (crear_servicio, editar_servicio, eliminar_servicio), el registro, login y logout propios, base.html con la barra de sesión y los mensajes, y el Web Service en Render. Cada reto muestra el código ANTES y DESPUÉS y cómo se ve en el navegador.
core/urls.py (path(), <int:pk>, name=, {% url %}, redirect con argumentos). Los retos 07, 08 y 09 van encadenados: 08 y 09 usan la ficha que se crea en el 07.ServicioForm, crear_servicio, editar_servicio, eliminar_servicio, registro, iniciar_sesion, cerrar_sesion, base.html). Si tu carpeta del proyecto se llama distinto, busca el mismo bloque. Cada reto trae «Cómo resolverlo en VS Code»; los comandos se escriben en la terminal integrada de VS Code, sin el $. Para practicar sin que los bloques de código te digan el archivo, usa la versión sin rutas (mismos 20 retos).
Antes de cualquier reto: tu terminal es la de VS Code
- Abre la carpeta del proyecto: Archivo → Abrir carpeta… y elige la que tiene
manage.py. - Abre la terminal integrada: menú Ver → Terminal o Ctrl + ñ (teclado en español; en inglés es Ctrl + `). Se abre abajo, ya ubicada en la carpeta del proyecto, con PowerShell.
- Revisa que diga
(venv)al inicio de la línea. Si no aparece, actívalo con el primer comando de abajo. - Abre archivos rápido con Ctrl + P y escribe el nombre (ej.
views.py). Guarda siempre con Ctrl + S antes de probar. - Ten dos cuentas listas (ej.
usuario_ayusuario_b) con servicios cargados: varios retos se prueban entrando con una y luego con la otra. Para probar «sin sesión» usa una ventana de incógnito (Ctrl + Shift + N).
# activa el entorno virtual (Windows, PowerShell de VS Code)
$ venv\Scripts\Activate.ps1
# si PowerShell bloquea el script, corre esto una vez y repite la línea anterior
$ Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass
# revisa que no haya errores de escritura en settings, urls, vistas ni formularios
$ python manage.py check
# levanta el servidor local para ver tus cambios en http://127.0.0.1:8000/
$ python manage.py runserverCRUD Dinámico y Sesiones (Semanas 5-6)
Demostrar en vivo que una cuenta nueva no ve los datos de otraoficial · lista de cotejo
Consigna del docente: “Registra una cuenta nueva ahora mismo, carga un servicio, cierra sesión, entra con tu cuenta de siempre y muéstrame que no ves el servicio que acabas de crear con la cuenta nueva.”
# @login_required: sin sesión no se ejecuta la vista
@login_required
# servicios: la vista del listado
def servicios(request):
# filter(propietario=request.user): WHERE propietario_id = <id del usuario de la sesión>
lista_servicios = Servicio.objects.filter(propietario=request.user)
# la tabla se dibuja solo con los servicios de esa cuenta
return render(request, 'core/servicios.html', {'servicios': lista_servicios})Cómo resolverlo en VS Code
- Abre tu URL de producción en
/registro/y creausuario_demo(contraseña de 8+ caracteres que no sea solo números). - Al registrarte entras directo: muestra el mensaje verde y la tabla vacía «Todavía no hay servicios.»
- Da clic en + Nuevo servicio, crea «Prueba demo» y muestra que aparece en la tabla.
- Da clic en Cerrar sesión (mensaje «Sesión cerrada correctamente.») y entra con tu cuenta de siempre en
/login/. - Muestra tu listado: «Prueba demo» no aparece.
- En VS Code: Ctrl + P → escribe
views.py→ Enter. Señala la línea delfiltery explica el WHERE.
Vista — la misma página /servicios/ con dos cuentas distintas
Hola, usuario_demo · Cerrar sesión
Servicios
| Servicio | Precio | Acciones |
|---|---|---|
| Prueba demo | $99.00 | Editar Eliminar |
Cuenta nueva: solo ve lo que ella creó
Hola, usuario_a · Cerrar sesión
Servicios
| Servicio | Precio | Acciones |
|---|---|---|
| Tutorías de programación | $150.00 | Editar Eliminar |
| Diseño de logotipos | $300.00 | Editar Eliminar |
«Prueba demo» no aparece en la otra cuenta ✓
- Registra la cuenta desde
/registro/, no desde/admin - Muestra el listado vacío de la cuenta nueva antes de crear nada
- Explica que
filter(propietario=request.user)agrega el WHERE con el id del usuario - Relaciona
request.usercon la cookie de sesión que dejó el login
📍 En tu práctica: tu URL de producción en Render y core/views.py, vista servicios.
Forzar el 404 al editar un servicio ajeno y explicarlo leyendo views.pyoficial · lista de cotejo
Consigna del docente: “Aquí tienes el id de un servicio que no es tuyo. Intenta editarlo desde la URL a mano y explica, leyendo views.py, por qué te da 404 en vez de mostrarte el formulario.”
# solo una condición: cualquier id existente se abría, aunque fuera de otra cuenta
servicio = get_object_or_404(Servicio, pk=pk)# dos condiciones: el id correcto Y que el dueño sea el usuario de la sesión
# si el servicio es de otra cuenta, la consulta no encuentra nada y responde 404
servicio = get_object_or_404(Servicio, pk=pk, propietario=request.user)Cómo resolverlo en VS Code
- En tu listado pasa el mouse sobre Editar y lee el número en la esquina inferior del navegador (ej.
/servicios/7/editar/). - En la barra de direcciones cambia ese número por el que te dicte el docente (ej.
/servicios/3/editar/) y presiona Enter. - Muestra el Not Found (en local con
DEBUG=Truedice «No Servicio matches the given query»). - En VS Code: Ctrl + P → escribe
views.py→ Enter. Con Ctrl + F buscaget_object_or_404y señalapropietario=request.user. - Repite con
/servicios/3/eliminar/: mismo candado, mismo 404.
Vista — escribir a mano el id de otra cuenta
Editar servicio
Titulo
Precio
Semana 5: se abría el servicio de OTRA cuenta ✗
Not Found
The requested resource was not found on this server.
Semana 6: 404, ni siquiera confirma que el id 3 existe ✓
- Cambia el id en la URL a mano, sin usar los botones
- Señala la segunda condición
propietario=request.user - Explica por qué 404 y no un aviso de «no es tuyo»: no revela que ese id existe
- Menciona que
eliminar_serviciotiene el mismo candado
📍 En tu práctica: core/views.py, funciones editar_servicio y eliminar_servicio (primera línea dentro de cada una).
Rechazar títulos de menos de 5 caracteres con clean_titulooficial · lista de cotejo
Consigna del docente: “Agrega una regla a ServicioForm que rechace títulos de menos de 5 caracteres, usando clean_titulo, y pruébala en el formulario.”
# clean_precio: la validación de la Semana 5
def clean_precio(self):
# ... (sus líneas no cambian)
# return: regresa el precio limpio
return precio # clean_precio: no se toca
def clean_precio(self):
# ... (sus líneas no cambian)
# return: regresa el precio limpio
return precio
# clean_titulo: NUEVO; Django lo llama solo porque se llama clean_ + nombre del campo
def clean_titulo(self):
# cleaned_data['titulo']: el texto que escribió el usuario
titulo = self.cleaned_data['titulo']
# len(titulo) < 5: cuenta los caracteres; menos de 5 no se acepta
if len(titulo) < 5:
# ValidationError: el formulario queda inválido y el mensaje sale junto al campo
raise ValidationError('El título debe tener al menos 5 caracteres.')
# return: sin esta línea el título llegaría vacío (None) y fallaría al guardar
return tituloCómo resolverlo en VS Code
- En VS Code: Ctrl + P → escribe
forms.py→ Enter. - Debajo de
clean_precioescribeclean_titulocon 4 espacios de sangría (dentro de la clase, al mismo nivel queclass Meta). - No agregues import:
ValidationErrorya está importado arriba desde la Semana 5. - Guarda con Ctrl + S (el punto blanco de la pestaña desaparece).
- En la terminal de VS Code (con
(venv)):python manage.py runservery abrehttp://127.0.0.1:8000/servicios/nuevo/en el navegador. Escribe «Web» → error rojo; escribe «Web pro» → se guarda.
Vista — /servicios/nuevo/ con el título «Web»
Nuevo servicio
Titulo
Se guardaba un título de 3 letras ✗
Nuevo servicio
Titulo
El título debe tener al menos 5 caracteres.
GuardarNo se guarda hasta corregir ✓
- Nombre exacto
clean_titulo, dentro de la claseServicioForm - Lee el valor desde
self.cleaned_data - Usa
raise ValidationError, no unprint - No olvida
return tituloy explica qué pasa sin él
📍 En tu práctica: core/forms.py, después de clean_precio (al final del archivo).
Cambiar la dirección /registro/ por /crear-cuenta/
Consigna del docente: “Quiero que el registro se abra en /crear-cuenta/. Cambia solo lo necesario y muéstrame que el enlace «Crear cuenta» de la barra sigue funcionando.”
# /registro/: pantalla de alta de cuenta
path('registro/', views.registro, name='registro'), # solo cambia el primer texto: lo que se escribe en el navegador
# la vista y el name='registro' se quedan iguales
path('crear-cuenta/', views.registro, name='registro'),Cómo resolverlo en VS Code
- En VS Code: Ctrl + P → escribe
urls.py→ Enter. Elige el de core (la carpeta se ve a la derecha del nombre). - Cambia solo
'registro/'por'crear-cuenta/'. - Guarda con Ctrl + S (el punto blanco de la pestaña desaparece).
- En la terminal de VS Code (con
(venv)):python manage.py runservery abrehttp://127.0.0.1:8000/login/en el navegador. Da clic en «Regístrate»: abre/crear-cuenta/. Prueba/registro/para mostrar el 404.
Vista — enlace «Crear cuenta» de la barra
Iniciar sesión · Crear cuenta
Crear cuenta
Nombre de usuario
La dirección vieja
Iniciar sesión · Crear cuenta
Crear cuenta
Nombre de usuario
Mismo enlace, nueva dirección, sin tocar plantillas ✓
- Distingue las 3 partes de
path(): dirección, vista yname - No toca
base.htmlnilogin.html - Explica que
{% url 'registro' %}busca porname, por eso los enlaces se actualizan solos
📍 En tu práctica: core/urls.py, la línea de name='registro'.
Crear la página pública /acerca/: vista, ruta y plantilla
Consigna del docente: “Agrega una página /acerca/ que diga el nombre de tu proyecto, que use base.html y que se pueda ver sin iniciar sesión.”
# /logout/: cierra la sesión activa
path('logout/', views.cerrar_sesion, name='logout'),
# cierra la lista urlpatterns
] # /logout/: no se toca
path('logout/', views.cerrar_sesion, name='logout'),
# /acerca/: NUEVA ruta; apunta a views.acerca y se llama 'acerca'
path('acerca/', views.acerca, name='acerca'),
# cierra la lista urlpatterns
]# acerca: NUEVA vista pública (sin @login_required)
def acerca(request):
# render sin contexto: la plantilla no necesita datos de la base
return render(request, 'core/acerca.html'){# extends: usa el molde base.html (barra de sesión y mensajes incluidos) #}
{% extends 'core/base.html' %}
{# block contenido: lo que va dentro del body de base.html #}
{% block contenido %}
{# h1: título visible de la página #}
<h1 class="text-2xl font-bold mb-4">Acerca de</h1>
{# p: texto libre; pon el nombre de tu proyecto #}
<p class="text-slate-600">Proyecto de Aplicaciones Web — UTH.</p>
{# endblock: fin del bloque contenido #}
{% endblock %}Cómo resolverlo en VS Code
- En VS Code: Ctrl + P → escribe
views.py→ Enter. Pega la vistaacercaal final. - En el Explorador de VS Code, clic derecho sobre
core/templates/core→ Nuevo archivo →acerca.html, y escribe la plantilla. - En VS Code: Ctrl + P → escribe
urls.py→ Enter. (el de core) y agrega elpathantes del]. - Guarda los 3 archivos (Ctrl + K y luego S guarda todos).
- En la terminal de VS Code (con
(venv)):python manage.py runservery abrehttp://127.0.0.1:8000/acerca/en el navegador. Ábrelo también en una ventana de incógnito (Ctrl + Shift + N) para mostrar que no pide login.
Vista — /acerca/ sin sesión
Page not found (404)
Todavía no existe la ruta ✗
Iniciar sesión · Crear cuenta
Acerca de
Proyecto de Aplicaciones Web — UTH.
Pública y con la barra de base.html ✓
- Toca los 3 archivos en orden lógico: vista, plantilla, ruta
- La plantilla empieza con
{% extends %} - Explica por qué no lleva
@login_required - Si sale
TemplateDoesNotExist, sabe revisar la carpetacore/templates/core/
📍 En tu práctica: core/views.py (final), core/urls.py (antes del ]) y un archivo nuevo en core/templates/core/.
Proteger /acerca/ con @login_required y explicar el ?next=
Consigna del docente: “Ahora haz que /acerca/ solo se vea con sesión iniciada. Ábrela sin sesión y explícame la dirección a la que te manda.”
⚠ Requiere el reto «Página pública /acerca/».
# acerca: NUEVA vista pública (sin @login_required)
def acerca(request):
# render sin contexto: la plantilla no necesita datos de la base
return render(request, 'core/acerca.html')# @login_required: NUEVO; sin sesión, manda a LOGIN_URL con ?next=/acerca/
@login_required
# acerca: la misma vista, ahora protegida
def acerca(request):
# render sin contexto: no cambia
return render(request, 'core/acerca.html')Cómo resolverlo en VS Code
- En VS Code: Ctrl + P → escribe
views.py→ Enter. Con Ctrl + F buscadef acerca. - Escribe
@login_requireden la línea de arriba, pegado al margen (sin sangría). El import ya existe. - Guarda con Ctrl + S (el punto blanco de la pestaña desaparece).
- Abre una ventana de incógnito (Ctrl + Shift + N) y ve a
http://127.0.0.1:8000/acerca/. - Lee en voz alta la dirección resultante:
/login/?next=/acerca/.
Vista — visitante sin sesión pide /acerca/
Iniciar sesión · Crear cuenta
Acerca de
Cualquiera la ve
Iniciar sesión · Crear cuenta
Iniciar sesión
Nombre de usuario
Contraseña
Redirigido al login ✓
- Coloca el decorador justo encima del
def - Explica que
/login/sale deLOGIN_URL = 'login'en settings.py - Explica que
?next=guarda la página que se pedía - Nota que tu
iniciar_sesionignoranexty siempre manda aservicios
📍 En tu práctica: core/views.py, encima de def acerca; LOGIN_URL al final de settings.py.
Crear la ficha de un servicio en /servicios/<pk>/
Consigna del docente: “Agrega una página que muestre un solo servicio completo, con descripción y categoría, en /servicios/<id>/. Que nadie pueda ver la ficha de otra cuenta.”
# /servicios/nuevo/: crear
path('servicios/nuevo/', views.crear_servicio, name='crear_servicio'), # /servicios/nuevo/: no se toca
path('servicios/nuevo/', views.crear_servicio, name='crear_servicio'),
# /servicios/<id>/: NUEVA; <int:pk> captura el número y lo pasa a la vista
path('servicios/<int:pk>/', views.detalle_servicio, name='detalle_servicio'),# @login_required: solo con sesión iniciada
@login_required
# detalle_servicio: recibe el pk desde la URL /servicios/<pk>/
def detalle_servicio(request, pk):
# mismo candado que editar: el id correcto Y que sea del usuario; si no, 404
servicio = get_object_or_404(Servicio, pk=pk, propietario=request.user)
# render: manda ese servicio a la plantilla de la ficha
return render(request, 'core/servicio_detalle.html', {'servicio': servicio}){# extends: usa el molde base.html #}
{% extends 'core/base.html' %}
{# block titulo: la pestaña muestra el título del servicio #}
{% block titulo %}{{ servicio.titulo }}{% endblock %}
{# block contenido: la ficha #}
{% block contenido %}
{# h1: título del servicio #}
<h1 class="text-2xl font-bold mb-2">{{ servicio.titulo }}</h1>
{# p: descripción completa #}
<p class="text-slate-600 mb-4">{{ servicio.descripcion }}</p>
{# p: precio y categoría en una línea #}
<p class="font-semibold">${{ servicio.precio }} · {{ servicio.categoria }}</p>
{# enlace de regreso al listado #}
<a href="{% url 'servicios' %}" class="inline-block mt-4 text-blue-600 underline">← Regresar</a>
{# endblock: fin del bloque contenido #}
{% endblock %}Cómo resolverlo en VS Code
- En VS Code: Ctrl + P → escribe
views.py→ Enter. Pegadetalle_servicioal final. - Crea
core/templates/core/servicio_detalle.html(clic derecho en la carpeta → Nuevo archivo). - En VS Code: Ctrl + P → escribe
urls.py→ Enter. (el de core) y agrega la ruta debajo deservicios/nuevo/. - Guarda todo y corre
python manage.py runserveren la terminal de VS Code. - Abre
/servicios/<tu id>/y luego el id de otra cuenta para mostrar el 404.
Vista — /servicios/7/
Page not found (404)
No existe la ruta ✗
Hola, usuario_a · Cerrar sesión
Tutorías de programación
Clases de Python de 1 hora por videollamada.
$150.00 · Tutorías
Ficha propia; la de otra cuenta da 404 ✓
- Usa
<int:pk>y la vista recibepk - Copia el candado
propietario=request.user(si no, abre IDOR) - Lleva
@login_required - Explica por qué
/servicios/nuevo/no choca: «nuevo» no es número
📍 En tu práctica: core/urls.py (debajo de crear_servicio), core/views.py (final) y un archivo nuevo en core/templates/core/.
Convertir el título de cada fila en enlace a su ficha
Consigna del docente: “En el listado, que al dar clic en el nombre del servicio se abra su ficha.”
⚠ Requiere el reto «Ficha /servicios/<pk>/».
{# td: celda con el título del servicio #}
<td class="px-4 py-2">{{ servicio.titulo }}</td> {# td: el título ahora es enlace; {% url %} arma /servicios/<id>/ con el pk de la fila #}
<td class="px-4 py-2"><a href="{% url 'detalle_servicio' servicio.pk %}" class="text-blue-600 underline">{{ servicio.titulo }}</a></td>Cómo resolverlo en VS Code
- En VS Code: Ctrl + P → escribe
servicios.html→ Enter. - Con Ctrl + F busca
servicio.tituloy envuélvelo en el<a>. - Guarda con Ctrl + S (el punto blanco de la pestaña desaparece).
- En la terminal de VS Code (con
(venv)):python manage.py runservery abrehttp://127.0.0.1:8000/servicios/en el navegador. Da clic en un título.
Vista — columna Servicio
Servicios
| Servicio | Precio | Acciones |
|---|---|---|
| Tutorías de programación | $150.00 | Editar Eliminar |
| Diseño de logotipos | $300.00 | Editar Eliminar |
Servicios
| Servicio | Precio | Acciones |
|---|---|---|
| Tutorías de programación | $150.00 | Editar Eliminar |
| Diseño de logotipos | $300.00 | Editar Eliminar |
Cada título abre su ficha ✓
- Usa
{% url 'detalle_servicio' servicio.pk %}, no la dirección a mano - Comillas simples adentro porque
hrefusa dobles - Sabe que
servicio.pkes el mismo número queservicio.id
📍 En tu práctica: core/templates/core/servicios.html, primera <td> dentro del {% for %}.
Después de crear un servicio, mandar a su ficha en vez del listado
Consigna del docente: “Cuando guarde un servicio nuevo, que me lleve directo a la ficha de ese servicio.”
⚠ Requiere el reto «Ficha /servicios/<pk>/».
# save(): guarda el registro completo en MySQL
nuevo.save()
# redirect('servicios'): regresa al listado
return redirect('servicios') # save(): después de esta línea, nuevo.pk ya tiene el id que asignó MySQL
nuevo.save()
# redirect con argumento: arma /servicios/<id>/ con el pk recién creado
return redirect('detalle_servicio', pk=nuevo.pk)Cómo resolverlo en VS Code
- En VS Code: Ctrl + P → escribe
views.py→ Enter. Buscadef crear_servicio. - Cambia solo el
return redirect(...)de dentro delif form.is_valid():. - Guarda con Ctrl + S (el punto blanco de la pestaña desaparece).
- En la terminal de VS Code (con
(venv)):python manage.py runservery abrehttp://127.0.0.1:8000/servicios/nuevo/en el navegador. Guarda uno y mira la dirección final.
Vista — justo después de «Guardar»
Servicios
| Servicio | Precio | Acciones |
|---|---|---|
| Tutorías de programación | $150.00 | Editar Eliminar |
| Diseño de logotipos | $300.00 | Editar Eliminar |
| Asesoría web | $500.00 | Editar Eliminar |
Asesoría web
Revisión de tu sitio en una sesión.
Llega a la ficha del nuevo id 8 ✓
- Pasa el
pkcomo argumento con nombre (pk=) - Explica que el id existe solo después del
save() - No cambia los
redirectde editar ni eliminar
📍 En tu práctica: core/views.py, función crear_servicio, dentro del if form.is_valid():.
Arreglar un NoReverseMatch: alguien borró la ruta de logout
Consigna del docente: “Borré una línea de tu proyecto y ahora el listado truena. Lee el error, encuentra qué falta y arréglalo.”
# /login/: pantalla de inicio de sesión propia
path('login/', views.iniciar_sesion, name='login'),
# cierra la lista urlpatterns (aquí faltaba la ruta de logout)
] # /login/: no se toca
path('login/', views.iniciar_sesion, name='login'),
# /logout/: RESTAURADA; base.html la busca con {% url 'logout' %}
path('logout/', views.cerrar_sesion, name='logout'),
# cierra la lista urlpatterns
]Cómo resolverlo en VS Code
- Lee la página amarilla de error: Reverse for 'logout' not found y la línea marcada de
base.html. - Explica: la plantilla pide una ruta con
name='logout'que ya no existe enurls.py. - En VS Code: Ctrl + P → escribe
urls.py→ Enter. (el de core) y vuelve a escribir elpathde logout. - Guarda con Ctrl + S (el punto blanco de la pestaña desaparece).
- Recarga F5 la página: el listado y la barra «Cerrar sesión» regresan.
Vista — /servicios/ con sesión iniciada
NoReverseMatch at /servicios/
Reverse for 'logout' not found. 'logout' is not a valid view function or pattern name.
Error en core/base.html, línea de {% url 'logout' %}
Hola, usuario_a · Cerrar sesión
Servicios
| Servicio | Precio | Acciones |
|---|---|---|
| Tutorías de programación | $150.00 | Editar Eliminar |
| Diseño de logotipos | $300.00 | Editar Eliminar |
La barra vuelve a armar /logout/ ✓
- Lee el error completo antes de tocar código
- Relaciona
{% url 'logout' %}con elname='logout' - Explica por qué el error sale aunque no visite
/logout/: la plantilla arma el enlace al dibujarse - Explica por qué sin sesión no truena: esa línea está dentro de
{% if user.is_authenticated %}
📍 En tu práctica: core/urls.py, después de la ruta login/.
Mostrar un mensaje verde «Servicio creado» al guardar uno nuevo
Consigna del docente: “Cuando cree un servicio, que aparezca un aviso verde con su nombre arriba del listado.”
# save(): guarda el registro completo en MySQL
nuevo.save()
# redirect: regresa al listado
return redirect('servicios') # save(): no se toca
nuevo.save()
# messages.success: NUEVO; se guarda ahora y se muestra una vez en la siguiente página
messages.success(request, f'Servicio "{nuevo.titulo}" creado.')
# redirect: el listado usa base.html, que ya dibuja los mensajes
return redirect('servicios')Cómo resolverlo en VS Code
- En VS Code: Ctrl + P → escribe
views.py→ Enter. Buscadef crear_servicio. - Entre
nuevo.save()y elreturn redirectagrega la línea demessages.success(misma sangría). - No agregues import:
from django.contrib import messagesya está desde la Semana 6. - Guarda con Ctrl + S (el punto blanco de la pestaña desaparece). En la terminal de VS Code (con
(venv)):python manage.py runservery abrehttp://127.0.0.1:8000/servicios/nuevo/en el navegador. Crea uno y luego recarga F5.
Vista — al regresar al listado
Hola, usuario_a · Cerrar sesión
Servicios
| Servicio | Precio | Acciones |
|---|---|---|
| Tutorías de programación | $150.00 | Editar Eliminar |
| Diseño de logotipos | $300.00 | Editar Eliminar |
Hola, usuario_a · Cerrar sesión
Servicios
| Servicio | Precio | Acciones |
|---|---|---|
| Tutorías de programación | $150.00 | Editar Eliminar |
| Diseño de logotipos | $300.00 | Editar Eliminar |
Desaparece al recargar ✓
- Pone el mensaje antes del
redirect - No toca la plantilla: explica que
base.htmlya tiene el{% for message in messages %} - Explica por qué desaparece al recargar
📍 En tu práctica: core/views.py, crear_servicio, dentro del if form.is_valid():.
Mostrar «Tienes N servicios» arriba de la tabla
Consigna del docente: “Arriba de la tabla quiero un texto que diga cuántos servicios tiene el usuario.”
{# h1: título de la página #}
<h1 class="text-2xl font-bold mb-6">Servicios</h1> {# h1: título de la página (mb-2 para dejar el contador pegado) #}
<h1 class="text-2xl font-bold mb-2">Servicios</h1>
{# |length cuenta los elementos de la lista; |pluralize agrega la "s" si no es 1 #}
<p class="mb-6 text-slate-600">Tienes {{ servicios|length }} servicio{{ servicios|length|pluralize }}</p>Cómo resolverlo en VS Code
- En VS Code: Ctrl + P → escribe
servicios.html→ Enter. - Debajo del
<h1>agrega el<p>con los filtros. - Guarda con Ctrl + S (el punto blanco de la pestaña desaparece).
- En la terminal de VS Code (con
(venv)):python manage.py runservery abrehttp://127.0.0.1:8000/servicios/en el navegador. Entra con dos cuentas: cada una ve su propio número.
Vista — arriba de la tabla
Servicios
| Servicio | Precio | Acciones |
|---|---|---|
| Tutorías de programación | $150.00 | Editar Eliminar |
| Diseño de logotipos | $300.00 | Editar Eliminar |
Servicios
Tienes 2 servicios
| Servicio | Precio | Acciones |
|---|---|---|
| Tutorías de programación | $150.00 | Editar Eliminar |
| Diseño de logotipos | $300.00 | Editar Eliminar |
Cuenta solo los del usuario ✓
- Reutiliza la variable
serviciosque ya manda la vista (no toca views.py) - Usa el filtro
|length - Explica que el número es por usuario gracias al
filter(propietario=...)
📍 En tu práctica: core/templates/core/servicios.html, debajo del <h1>.
Agregar la columna Categoría a la tabla del listado
Consigna del docente: “Quiero ver la categoría de cada servicio en la tabla, entre Precio y Acciones.”
{# th: encabezado de la columna del precio #}
<th class="text-left px-4 py-2">Precio</th>
{# ··· más abajo, dentro del for ··· #}
<td class="px-4 py-2">${{ servicio.precio }}</td>
{# ··· y en el empty ··· #}
<tr><td colspan="3" class="px-4 py-6 text-center text-slate-500">Todavía no hay servicios.</td></tr> {# th: encabezado del precio (no se toca) #}
<th class="text-left px-4 py-2">Precio</th>
{# th: NUEVO encabezado #}
<th class="text-left px-4 py-2">Categoría</th>
{# td: precio (no se toca) #}
<td class="px-4 py-2">${{ servicio.precio }}</td>
{# td: NUEVA celda con el campo categoria del modelo #}
<td class="px-4 py-2">{{ servicio.categoria }}</td>
{# colspan="4": ahora son 4 columnas, el aviso debe abarcarlas todas #}
<tr><td colspan="4" class="px-4 py-6 text-center text-slate-500">Todavía no hay servicios.</td></tr>Cómo resolverlo en VS Code
- En VS Code: Ctrl + P → escribe
servicios.html→ Enter. - Agrega el
<th>después de «Precio» en el<thead>. - Agrega el
<td>después de la celda del precio, dentro del{% for %}. - Cambia
colspan="3"porcolspan="4"en el{% empty %}. - Guarda con Ctrl + S (el punto blanco de la pestaña desaparece). En la terminal de VS Code (con
(venv)):python manage.py runservery abrehttp://127.0.0.1:8000/servicios/en el navegador.
Vista — la tabla
Servicios
| Servicio | Precio | Acciones |
|---|---|---|
| Tutorías de programación | $150.00 | Editar Eliminar |
| Diseño de logotipos | $300.00 | Editar Eliminar |
Servicios
| Servicio | Precio | Categoría | Acciones |
|---|---|---|---|
| Tutorías de programación | $150.00 | Tutorías | Editar Eliminar |
| Diseño de logotipos | $300.00 | Diseño | Editar Eliminar |
Encabezado y celda en la misma posición ✓
- Agrega encabezado y celda en la misma posición
- Usa el nombre exacto del campo:
categoriasin acento - Se acuerda del
colspan
📍 En tu práctica: core/templates/core/servicios.html: <thead>, la fila del {% for %} y el {% empty %}.
Ordenar el listado del más nuevo al más viejo
Consigna del docente: “Que el último servicio que creé salga hasta arriba de la tabla.”
# filter: solo los servicios del usuario de la sesión
lista_servicios = Servicio.objects.filter(propietario=request.user) # filter: no cambia · order_by('-creado_en'): ORDER BY creado_en DESC (el "-" invierte el orden)
lista_servicios = Servicio.objects.filter(propietario=request.user).order_by('-creado_en')Cómo resolverlo en VS Code
- En VS Code: Ctrl + P → escribe
views.py→ Enter. Buscadef servicios. - Al final de la línea del
filter(...)agrega.order_by('-creado_en'). - Guarda con Ctrl + S (el punto blanco de la pestaña desaparece).
- En la terminal de VS Code (con
(venv)):python manage.py runservery abrehttp://127.0.0.1:8000/servicios/nuevo/en el navegador. Crea uno y comprueba que queda primero.
Vista — después de crear «Asesoría web»
Servicios
| Servicio | Precio | Acciones |
|---|---|---|
| Tutorías de programación | $150.00 | Editar Eliminar |
| Diseño de logotipos | $300.00 | Editar Eliminar |
| Asesoría web | $500.00 | Editar Eliminar |
El nuevo queda al final
Servicios
| Servicio | Precio | Acciones |
|---|---|---|
| Asesoría web | $500.00 | Editar Eliminar |
| Tutorías de programación | $150.00 | Editar Eliminar |
| Diseño de logotipos | $300.00 | Editar Eliminar |
El nuevo queda arriba ✓
- Encadena
order_bydespués delfiltersin perder el filtro - Explica el
-(descendente) - Usa
creado_en, el campo conauto_now_add
📍 En tu práctica: core/views.py, función servicios.
Quitar @login_required del listado, mostrar el error y restaurarlo
Consigna del docente: “Comenta el @login_required de la vista servicios, abre el listado sin sesión, explícame el error y déjalo como estaba.”
# @login_required ← comentado solo para la prueba
# servicios: la vista del listado
def servicios(request):
# sin sesión, request.user es AnonymousUser, que no tiene id
lista_servicios = Servicio.objects.filter(propietario=request.user)# @login_required: corta ANTES de llegar al filter y manda al login
@login_required
# servicios: la vista del listado
def servicios(request):
# aquí request.user siempre es un usuario real con id
lista_servicios = Servicio.objects.filter(propietario=request.user)Cómo resolverlo en VS Code
- En VS Code: Ctrl + P → escribe
views.py→ Enter. Buscadef servicios. - Pon el cursor en la línea
@login_requiredy presiona Ctrl + } (o Ctrl + /) para comentarla. Guarda. - Abre una ventana de incógnito (Ctrl + Shift + N) en
http://127.0.0.1:8000/servicios/y lee el error. - Descomenta con el mismo atajo, guarda y recarga: ahora te manda a
/login/?next=/servicios/.
Vista — /servicios/ sin sesión (ventana de incógnito)
TypeError at /servicios/
Field 'id' expected a number but got <AnonymousUser>.
En producción sería un Server Error (500) ✗
Iniciar sesión · Crear cuenta
Iniciar sesión
Nombre de usuario
Redirige al login ✓
- Explica que sin sesión
request.useresAnonymousUsery no tiene id - Explica que el decorador corta antes de ejecutar la vista
- Lo deja restaurado y prueba el redirect a
/login/?next=
📍 En tu práctica: core/views.py, línea de arriba de def servicios.
Quitar {% csrf_token %}, mostrar el 403 y restaurarlo
Consigna del docente: “Borra el csrf_token del formulario, intenta guardar, explícame el error y déjalo como estaba.”
{# method="POST": este formulario modifica la base de datos #}
<form method="POST" class="space-y-4 max-w-md">
{# form.as_p: los campos (aquí faltaba el csrf_token) #}
{{ form.as_p }} {# method="POST": este formulario modifica la base de datos #}
<form method="POST" class="space-y-4 max-w-md">
{# csrf_token: campo oculto con el token que Django revisa en cada POST #}
{% csrf_token %}
{# form.as_p: los campos del ModelForm #}
{{ form.as_p }}Cómo resolverlo en VS Code
- En VS Code: Ctrl + P → escribe
servicio_form.html→ Enter. - Corta la línea
{% csrf_token %}con Ctrl + X y guarda. - En la terminal de VS Code (con
(venv)):python manage.py runservery abrehttp://127.0.0.1:8000/servicios/nuevo/en el navegador. Llena el formulario y da Guardar: sale el 403. - Pega de nuevo la línea con Ctrl + V, guarda, recarga F5 la página del formulario y guarda: ahora sí funciona.
- Opcional: Ctrl + U en el navegador para ver el
<input type="hidden" name="csrfmiddlewaretoken">.
Vista — clic en «Guardar»
Forbidden (403)
CSRF verification failed. Request aborted. Reason: CSRF token missing.
Servicios
| Servicio | Precio | Acciones |
|---|---|---|
| Tutorías de programación | $150.00 | Editar Eliminar |
| Diseño de logotipos | $300.00 | Editar Eliminar |
| Asesoría web | $500.00 | Editar Eliminar |
Con el token, el POST se acepta ✓
- Explica qué ataque evita (envío desde otro sitio con tu sesión)
- Sabe que lo revisa el middleware en cada POST
- Lo deja restaurado y recarga el formulario antes de probar
📍 En tu práctica: core/templates/core/servicio_form.html, justo debajo de <form method="POST">.
Mostrar en /admin que la contraseña está hasheada
Consigna del docente: “Entra al panel de administración y enséñame cómo guardó Django la contraseña de usuario_a. ¿Me la puedes decir?”
# algoritmo: pbkdf2_sha256 · iteraciones: 870000 · sal: aleatoria · hash: resultado
algorithm: pbkdf2_sha256 iterations: 870000 salt: Xy3k****** hash: Qm9v**********Cómo resolverlo en VS Code
- En la terminal de VS Code (con
(venv)):python manage.py runservery abrehttp://127.0.0.1:8000/admin/en el navegador. Entra con tu superusuario. - Ve a Usuarios →
usuario_a. - Señala el campo Contraseña: algoritmo, iteraciones, sal y hash (Django oculta parte con asteriscos).
- Explica que no hay forma de leer la original; el botón del admin solo permite cambiarla.
Vista — ficha del usuario en /admin
Seleccione usuario a modificar
| Nombre de usuario | Es staff |
|---|---|
| usuario_a | ✗ |
| usuario_demo | ✗ |
Contraseña:
algorithm: pbkdf2_sha256 iterations: 870000 salt: Xy3k****** hash: Qm9v**********
No se guarda la contraseña, solo su hash ✓
- Dice que no se puede desencriptar (función de un solo sentido)
- Nombra el algoritmo y la sal
- Explica cómo se valida el login: se hashea lo tecleado y se compara
📍 En tu práctica: /admin → Autenticación y autorización → Usuarios.
Agregar el enlace «Mis servicios» en la barra, solo con sesión iniciada
Consigna del docente: “En la barra de arriba quiero un enlace a Mis servicios, pero que solo aparezca si ya inicié sesión.”
{% if user.is_authenticated %}
{# user.username: nombre del usuario que inició sesión #}
<span>Hola, <b>{{ user.username }}</b></span> {% if user.is_authenticated %}
{# NUEVO: enlace al listado; {% url %} usa el name='servicios' que ya existe #}
<a href="{% url 'servicios' %}" class="text-blue-600 underline">Mis servicios</a>
{# user.username: no se toca #}
<span>Hola, <b>{{ user.username }}</b></span>Cómo resolverlo en VS Code
- En VS Code: Ctrl + P → escribe
base.html→ Enter. - Dentro del
{% if user.is_authenticated %}, arriba del «Hola», agrega el<a>. - Guarda con Ctrl + S (el punto blanco de la pestaña desaparece).
- En la terminal de VS Code (con
(venv)):python manage.py runservery abrehttp://127.0.0.1:8000/servicios/nuevo/en el navegador. Cierra sesión y mira que el enlace desaparece.
Vista — la barra superior
Hola, usuario_a · Cerrar sesión
Nuevo servicio
Mis servicios · Hola, usuario_a · Cerrar sesión
Nuevo servicio
Solo aparece con sesión ✓
- Lo pone dentro del
if, no delelse - Usa
{% url 'servicios' %} - Explica que al ser
base.htmlaparece en todas las páginas
📍 En tu práctica: core/templates/core/base.html, dentro de <nav>.
Aviso amarillo al eliminar (nivel warning) y colores según el tipo de mensaje
Consigna del docente: “Cuando borre un servicio, que salga un aviso, pero amarillo, no verde como los demás.”
# delete(): borra la fila de MySQL
servicio.delete()
# redirect: regresa al listado
return redirect('servicios') # delete(): no se toca
servicio.delete()
# messages.warning: NUEVO; mismo mecanismo, pero con la etiqueta "warning"
messages.warning(request, 'Servicio eliminado.')
# redirect: no se toca
return redirect('servicios') {# for: recorre los mensajes pendientes #}
{% for message in messages %}
{# message.tags: "success", "info" o "warning", según la función usada en la vista #}
{% if message.tags == 'warning' %}
{# warning: caja amarilla #}
<div class="bg-amber-100 text-amber-800 px-4 py-2 rounded-lg mb-4">{{ message }}</div>
{# cualquier otro tipo #}
{% else %}
{# success / info: la caja verde de siempre #}
<div class="bg-emerald-100 text-emerald-800 px-4 py-2 rounded-lg mb-4">{{ message }}</div>
{# fin de la condición #}
{% endif %}
{# fin de la repetición #}
{% endfor %}Cómo resolverlo en VS Code
- En VS Code: Ctrl + P → escribe
views.py→ Enter. Eneliminar_servicio, entredelete()y elredirect, agregamessages.warning. - En VS Code: Ctrl + P → escribe
base.html→ Enter. Reemplaza el bloque{% for message in messages %}por el de abajo. - Guarda los 2 archivos.
- En la terminal de VS Code (con
(venv)):python manage.py runservery abrehttp://127.0.0.1:8000/servicios/en el navegador. Elimina uno y crea otro para comparar colores.
Vista — después de eliminar
Hola, usuario_a · Cerrar sesión
Servicios
| Servicio | Precio | Acciones |
|---|---|---|
| Tutorías de programación | $150.00 | Editar Eliminar |
Se borró sin avisar
Hola, usuario_a · Cerrar sesión
Servicios
| Servicio | Precio | Acciones |
|---|---|---|
| Tutorías de programación | $150.00 | Editar Eliminar |
Amarillo para warning, verde para lo demás ✓
- Usa
messages.warning, no otro color en la vista - Decide el color en la plantilla con
message.tags - Explica que la vista dice qué pasó y la plantilla cómo se ve
📍 En tu práctica: core/views.py (eliminar_servicio) y core/templates/core/base.html (bloque de mensajes).
Subir un cambio y probarlo en producción (y resolver un 403 de CSRF)
Consigna del docente: “Sube el cambio que acabas de hacer, muéstrame el deploy en Render y pruébalo en tu URL pública. Si el login te da 403, arréglalo.”
# CSRF_TRUSTED_ORIGINS: tu dirección completa CON https:// y SIN / al final
CSRF_TRUSTED_ORIGINS=https://u3s6-hernandez-nunez-maria-jose.onrender.comCómo resolverlo en VS Code
- Abre la terminal de VS Code (Ctrl + ñ) y corre los comandos de abajo, uno por uno.
- En Render abre tu Web Service → Events: aparece un deploy nuevo con tu mensaje de commit.
- En Logs espera
Your service is live 🎉. - Abre tu URL pública, inicia sesión y prueba el cambio.
- Si el login responde 403 CSRF verification failed: Environment → revisa
CSRF_TRUSTED_ORIGINSconhttps://→ Save, rebuild, and deploy.
# confirma que tu proyecto apunta a tu repo u3s6-…
$ git remote -v
# lista lo que cambiaste: NO debe aparecer .env
$ git status
# agrega los cambios al siguiente commit
$ git add .
# crea el commit con un mensaje que explica el cambio
$ git commit -m "U3: live coding en vivo"
# sube a GitHub; Render detecta el push y redespliega solo
$ git pushVista — iniciar sesión en producción
Forbidden (403)
CSRF verification failed. Request aborted. Origin checking failed.
Falta https:// en CSRF_TRUSTED_ORIGINS ✗
Hola, usuario_a · Cerrar sesión
Servicios
Deploy nuevo y login funcionando ✓
- Revisa
git statusantes deladd(sin.env) - Muestra en Events que el push disparó el deploy
- Sabe que las variables viven en Render, no en el repo
- Explica qué protege
CSRF_TRUSTED_ORIGINS
📍 En tu práctica: terminal integrada de VS Code en la carpeta con manage.py; en Render: tu Web Service → Events, Logs y Environment.