Banco de Preguntas Conceptuales — Unidad 3
Las 20 preguntas del “Examen conceptual rápido” de la Unidad 3 (CRUD con ModelForms, {% csrf_token %}, autenticación, sesiones, aislamiento de datos por usuario y checklist de seguridad; Semanas 5-6). Cada una trae una respuesta modelo breve, las palabras clave mínimas que tu respuesta hablada debe incluir y, cuando aplica, el fragmento de tu propio código donde se ve ese concepto. Se complementan con los 20 retos de live coding de la Unidad 3 y los 20 retos sin rutas.
CRUD Dinámico y Sesiones
¿Qué genera automáticamente un ModelForm a partir de un modelo, y qué seguiría teniendo que escribirse a mano?
Respuesta modelo: Con class Meta: model = Servicio y fields = [...] Django genera solo los campos HTML (un <input> o <textarea> por campo, según su tipo), sus etiquetas y las validaciones del modelo (obligatorio, max_length, número decimal). A mano sigues escribiendo la vista (GET/POST, is_valid(), save(), redirect), la plantilla con <form method="POST"> y {% csrf_token %}, la ruta en urls.py, las reglas propias (clean_precio) y asignar el propietario.
# ServicioForm: hereda de ModelForm, así Django arma los campos solo
class ServicioForm(forms.ModelForm):
# Meta: le dice a Django de qué modelo sale el formulario
class Meta:
# model: los campos se generan a partir de Servicio
model = Servicio
# fields: solo estos 4 aparecen en pantalla; propietario queda fuera
fields = ['titulo', 'descripcion', 'precio', 'categoria']¿Para qué sirve {% csrf_token %} y qué pasaría si un formulario POST no lo incluyera?
Respuesta modelo: Inserta un campo oculto con un token secreto ligado a la cookie del navegador. Al recibir el POST, el middleware CsrfViewMiddleware compara el token: así Django sabe que el formulario salió de tu propio sitio y no de una página ajena que intenta enviar datos usando la sesión de la víctima (ataque CSRF, falsificación de petición entre sitios). Si falta, Django rechaza el envío con 403 Forbidden — CSRF verification failed.
{# 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; sin él, Django responde 403 #}
{% csrf_token %}¿Cómo guarda Django una contraseña — se puede “desencriptar” para verla? Explica qué es el hashing.
Respuesta modelo: Django nunca guarda la contraseña en texto plano. Le aplica una función hash de un solo sentido (por defecto PBKDF2 con SHA-256, muchas iteraciones y una sal aleatoria) y guarda solo el resultado, con el formato pbkdf2_sha256$iteraciones$sal$hash en la tabla auth_user. No se puede desencriptar: para hacer login, Django vuelve a hashear lo que el usuario tecleó con la misma sal y compara los dos resultados.
# así se ve lo que guarda MySQL en auth_user.password (nunca la contraseña real)
pbkdf2_sha256$870000$Xy3k...sal...$Qm9v...hash...=¿Qué diferencia hay entre Servicio.objects.all() y Servicio.objects.filter(propietario=request.user)?
Respuesta modelo: .all() genera SELECT ... FROM core_servicio: trae los servicios de todos los usuarios. .filter(propietario=request.user) agrega WHERE propietario_id = <id del usuario logueado>: solo trae los del usuario que hizo la petición. request.user lo llena Django a partir de la cookie de sesión. Es la línea que hace que “cada quien vea solo lo suyo” en el listado.
# filter(propietario=request.user): SELECT ... WHERE propietario_id = <id del usuario logueado>
lista_servicios = Servicio.objects.filter(propietario=request.user)¿Qué es una referencia insegura a objetos (IDOR) y cómo la evita get_object_or_404(Servicio, pk=pk, propietario=request.user)?
Respuesta modelo: IDOR (Insecure Direct Object Reference) es cuando basta con cambiar el número de la URL (/servicios/7/editar/ → /servicios/8/editar/) para ver o modificar un registro ajeno. Con dos condiciones — el pk correcto y que el propietario sea el usuario actual — si el servicio es de otra persona la consulta no encuentra nada y Django responde 404: ni siquiera confirma que ese id exista.
# dos condiciones: el id correcto Y que el dueño sea el usuario actual; si no, 404
servicio = get_object_or_404(Servicio, pk=pk, propietario=request.user)Entiende el concepto
IDOR = el sistema confía en el número que viene en la URL sin preguntar si ese registro es tuyo.
Compáralo con un hotel: tu llave abre el cuarto 7. Si la cerradura del cuarto 8 también abriera con tu llave solo porque escribiste "8" en la puerta, cualquiera entraría a cualquier cuarto. Lo correcto es que la cerradura revise dos cosas: el número del cuarto y que la llave sea del huésped de ese cuarto.
Paso a paso de cómo pasa:
- Inicias sesión como
usuario_ay editas tu servicio: la dirección es/servicios/7/editar/. - Ese
7es elid(la llave primaria,pk) del registro en la tablacore_servicio. Los ids son números seguidos: 7, 8, 9… - En la barra del navegador cambias el 7 por un 8. Nadie te lo impide: la URL la escribe el usuario.
- Si la vista solo busca por
pk, Django encuentra el servicio 8 aunque sea deusuario_b, y puedes verlo, editarlo o borrarlo. Eso es IDOR.
✗ Vulnerable: una sola condición
get_object_or_404(Servicio, pk=pk)
SQL que genera: SELECT … FROM core_servicio WHERE id = 8
Resultado: encuentra el servicio de otra persona y te lo deja editar.
✓ Seguro: dos condiciones
get_object_or_404(Servicio, pk=pk, propietario=request.user)
SQL que genera: SELECT … FROM core_servicio WHERE id = 8 AND propietario_id = 3
Resultado: el servicio 8 no es del usuario 3 → la consulta no devuelve nada → 404.
¿Por qué 404 y no "403 Prohibido"? Un 403 confirmaría que el servicio 8 sí existe y que es de alguien más; con eso un atacante podría ir probando números para saber cuántos registros hay. El 404 responde "para ti, eso no existe", y no revela nada.
¿De dónde sale request.user? De la cookie de sesión que Django guardó al iniciar sesión. El usuario no lo puede cambiar escribiendo en la URL ni en el formulario: por eso es la condición confiable.
Error común: poner el candado solo en editar_servicio y olvidarlo en eliminar_servicio. Cada vista que reciba un pk en la URL necesita su propia segunda condición.
Cómo demostrarlo en vivo: entra como usuario_a, abre /servicios/<id de un servicio de usuario_b>/editar/ y muestra que sale «Page not found (404)».
¿Por qué propietario no aparece en fields del ServicioForm?
Respuesta modelo: Porque no debe decidirlo quien llena el formulario. Si estuviera en fields, saldría una lista con todos los usuarios y cualquiera podría crear (o “regalar”) un servicio a nombre de otro, incluso alterando el HTML. Lo asigna la vista, en el servidor, con nuevo.propietario = request.user, que sale de la sesión y no se puede falsificar desde el navegador.
¿Qué hace form.save(commit=False) y por qué lo usas en crear_servicio?
Respuesta modelo: Crea el objeto Servicio en memoria con los datos del formulario, sin hacer el INSERT en MySQL todavía. Lo necesitas porque al formulario le falta el propietario (no está en fields) y la columna propietario_id es obligatoria: primero asignas nuevo.propietario = request.user y después nuevo.save() guarda el registro completo.
# commit=False: arma el Servicio en memoria SIN guardarlo en MySQL
nuevo = form.save(commit=False)
# completa el dato que el formulario no trae
nuevo.propietario = request.user
# ahora sí: INSERT con todos los campos
nuevo.save()Una misma vista atiende GET y POST. ¿Qué hace en cada caso?
Respuesta modelo: Con GET (el usuario abre la página) crea un formulario vacío — o precargado con instance= al editar — y lo dibuja. Con POST (el usuario dio clic en «Guardar») reconstruye el formulario con request.POST, lo valida con is_valid(): si pasa, guarda y redirige; si no, vuelve a dibujar la misma plantilla con los errores y lo que el usuario ya había escrito.
Entiende el concepto
GET y POST son dos formas en que el navegador le habla al servidor: GET pide una página y POST entrega datos. La misma dirección (/servicios/nuevo/) recibe las dos, y la vista decide qué hacer leyendo request.method.
Compáralo con una ventanilla de trámites: la primera vez que llegas pides el formato en blanco (GET). Lo llenas y lo entregas en la misma ventanilla (POST). El empleado lo revisa: si está completo, lo sella y te manda a la sala de espera (guardar y redirigir); si le falta algo, te regresa el mismo formato con lo que ya escribiste y te marca en rojo lo que falta (errores en la misma plantilla).
Paso a paso de las dos visitas a la misma URL:
- Primera visita (GET): escribes la dirección o das clic en «+ Nuevo servicio». El navegador hace un GET. La vista entra al
else, creaServicioForm()vacío y lo dibuja. Al editar se usaServicioForm(instance=servicio), que llega precargado con los datos actuales. - Segunda visita (POST): llenas los campos y das «Guardar». El
<form method="POST">manda los datos a la misma URL. La vista entra alif request.method == 'POST'y reconstruye el formulario conServicioForm(request.POST). - Validación:
is_valid()revisa campos obligatorios, tipos,max_lengthy tuclean_precio. - Si es válido: guarda en MySQL y hace
redirect('servicios'), que lleva al listado con un GET nuevo (patrón Post/Redirect/Get, pregunta 09). - Si no es válido: no guarda nada, y el
renderdel final vuelve a dibujar la misma plantilla con el formulario que ya trae lo escrito y un mensaje rojo junto a cada campo con error.
# la misma función atiende GET y POST de /servicios/nuevo/
def crear_servicio(request):
# POST = el usuario envió el formulario con «Guardar»
if request.method == 'POST':
# reconstruye el formulario con lo que el usuario tecleó
form = ServicioForm(request.POST)
# valida: obligatorios, tipos, max_length y clean_precio
if form.is_valid():
# arma el objeto sin guardarlo todavía
nuevo = form.save(commit=False)
# el dueño lo pone la vista con la sesión
nuevo.propietario = request.user
# INSERT en MySQL
nuevo.save()
# válido: va al listado con un GET nuevo
return redirect('servicios')
# GET = el usuario apenas abrió la página
else:
# formulario vacío
form = ServicioForm()
# llega aquí en GET (form vacío) o en POST inválido (form con datos y errores)
return render(request, 'core/servicio_form.html', {'form': form})La línea clave es el render del final: está fuera del if y del else, así que la usan dos casos distintos. En GET dibuja el formulario vacío; en un POST inválido dibuja el formulario con los datos y los errores. Por eso el usuario no pierde lo que ya escribió.
Error común: poner form = ServicioForm() antes del if o dentro del POST cuando falla la validación. Así el formulario de errores se reemplaza por uno vacío: el usuario pierde lo que escribió y nunca ve el mensaje rojo.
Cómo demostrarlo en vivo: abre «Nuevo servicio» (GET, campos vacíos), escribe un precio de 0 y da «Guardar» (POST inválido: sale «El precio debe ser mayor a 0.» y lo demás sigue escrito). Corrige el precio y guarda (POST válido: te lleva al listado).
¿Por qué después de guardar haces return redirect('servicios') en vez de render?
Respuesta modelo: Es el patrón Post/Redirect/Get. Si respondieras con render al POST, el navegador se queda “parado” en ese POST y al recargar (F5) volvería a enviar el formulario, creando un registro duplicado. Con redirect el navegador hace un GET nuevo al listado, así recargar ya no repite el envío.
En editar_servicio, ¿qué cambia al pasar instance=servicio al formulario?
Respuesta modelo: Le dice al ModelForm “trabaja sobre este registro”. En el GET, precarga los campos con los valores actuales. En el POST, form.save() hace un UPDATE de esa fila en vez de un INSERT. Sin instance=, cada “edición” crearía un servicio nuevo.
# instance=servicio: el formulario actualiza ESE registro en vez de crear uno nuevo
form = ServicioForm(request.POST, instance=servicio)¿Por qué eliminar_servicio solo borra si llega un POST y con un GET muestra «¿Seguro?»?
Respuesta modelo: Un GET no debe cambiar datos: lo disparan enlaces, el historial, la precarga del navegador o una imagen con esa URL en otra página. Si un GET borrara, cualquier visita accidental eliminaría registros. Con POST el borrado solo ocurre tras el clic en «Sí, eliminar», y además ese POST va protegido por {% csrf_token %}.
Explica cómo funciona tu clean_precio: ¿quién lo llama, qué es cleaned_data y qué hace ValidationError?
Respuesta modelo: Django lo llama solo durante is_valid(), porque el nombre sigue la regla clean_<campo>. self.cleaned_data['precio'] trae el valor ya convertido a Decimal. Si es <= 0, raise ValidationError(...) marca el formulario como inválido y el mensaje aparece en rojo junto al campo. Si pasa, debe regresar el valor con return precio.
# clean_precio: Django lo llama solo al validar el campo "precio"
def clean_precio(self):
# cleaned_data: valor ya convertido a Decimal
precio = self.cleaned_data['precio']
# regla propia: rechazar cero o negativos
if precio <= 0:
# el formulario queda inválido y se muestra este mensaje
raise ValidationError('El precio debe ser mayor a 0.')
# siempre se regresa el valor limpio
return precio¿Qué hace @login_required y qué papel juega LOGIN_URL en settings.py?
Respuesta modelo: Es un decorador: antes de ejecutar la vista revisa request.user.is_authenticated. Si no hay sesión, no corre la vista y redirige a LOGIN_URL (en tu proyecto, 'login' → /login/) agregando ?next=/la/pagina/que/pedía/. En la Semana 5 apuntaba a /admin/login/; en la Semana 6 a tu login propio.
# LOGIN_URL: a dónde manda @login_required a un visitante sin sesión
LOGIN_URL = 'login'¿Qué diferencia hay entre UserCreationForm y AuthenticationForm?
Respuesta modelo: Los dos vienen en django.contrib.auth.forms. UserCreationForm es el de registro: pide usuario y contraseña dos veces, valida que el usuario no exista y que la contraseña cumpla las reglas, y su save() crea el User con la contraseña hasheada. AuthenticationForm es el de login: recibe request y los datos, comprueba usuario y contraseña contra la base, y con get_user() entrega el usuario verificado para pasarlo a login().
¿Qué hacen login(request, usuario) y logout(request)? ¿Qué es la cookie sessionid?
Respuesta modelo: login() crea una sesión en el servidor (tabla django_session) con el id del usuario y manda al navegador una cookie sessionid con un identificador aleatorio. En cada petición siguiente el navegador envía esa cookie y Django llena request.user. logout() borra esa sesión, así que request.user vuelve a ser AnonymousUser.
¿Qué son los mensajes de django.contrib.messages y por qué se ven solo una vez?
Respuesta modelo: Son mensajes flash: la vista los guarda con messages.success(request, '...') o messages.info(...) antes del redirect, y se muestran en la siguiente página con {% for message in messages %} en base.html. Al recorrerlos en la plantilla se marcan como leídos y se borran, por eso desaparecen al recargar.
En base.html, ¿cómo decide la barra si mostrar «Hola, usuario / Cerrar sesión» o «Iniciar sesión / Crear cuenta»?
Respuesta modelo: Con {% if user.is_authenticated %}. La variable user llega sola a todas las plantillas gracias al context processor auth, y es el mismo request.user. Si hay sesión es un User real (is_authenticated = True) y se muestra {{ user.username }}; si no, es AnonymousUser y entra el {% else %}.
{# True si hay sesión iniciada, False si el visitante es anónimo #}
{% if user.is_authenticated %}
{# user.username: el nombre del usuario de la sesión #}
<span>Hola, <b>{{ user.username }}</b></span>¿Para qué sirve base.html con {% block contenido %} y qué hace {% extends 'core/base.html' %}?
Respuesta modelo: base.html es el molde que comparten todas las páginas (head, Tailwind, barra de sesión, mensajes). Marca huecos con {% block %}. Cada plantilla hija pone {% extends %} como primera etiqueta y solo rellena esos bloques. Ventaja: cambias la barra en un solo archivo y se actualiza en listado, formularios, login y registro (DRY, no repetirse).
¿Qué hace <int:pk> en path('servicios/<int:pk>/editar/', ...) y cómo arma esa dirección {% url 'editar_servicio' servicio.pk %}?
Respuesta modelo: <int:pk> es un convertidor: captura el número de la URL, lo convierte a entero y lo pasa a la vista como argumento pk (def editar_servicio(request, pk)). Si no es número, ni siquiera entra (404). En la plantilla, {% url %} busca la ruta por su name= y pone servicio.pk en el hueco: produce /servicios/7/editar/ sin escribir la dirección a mano.
# <int:pk>: captura el número de la URL y lo pasa como pk a la vista
path('servicios/<int:pk>/editar/', views.editar_servicio, name='editar_servicio'),En producción usas DEBUG=False, una SECRET_KEY propia, ALLOWED_HOSTS y CSRF_TRUSTED_ORIGINS. ¿Para qué sirve cada una?
Respuesta modelo: DEBUG=False: ante un error muestra una página genérica, no tu código ni tus variables. SECRET_KEY distinta a la local: firma las cookies de sesión y los tokens CSRF; si se filtra, alguien podría falsificar sesiones. ALLOWED_HOSTS con el dominio exacto (sin https://): rechaza peticiones con un Host falso. CSRF_TRUSTED_ORIGINS con https://: sin ella, Django responde 403 a los formularios de login y registro servidos por HTTPS en Render.