APLICAWEB 4to/Propuesta Intensiva 8 Semanas/Anexos/Banco de Preguntas · Unidad 3
20 preguntas · Unidad 3
✓

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.

Cómo se califica: el docente escucha tu respuesta en voz alta (sin leer) y marca “correcta” si cumples el mínimo indicado en el criterio de cada pregunta, explicando la relación entre los conceptos; no basta con nombrarlos sueltos. Las preguntas 1 a 5 son las oficiales de la lista de cotejo (3.4); las 6 a 20 son de refuerzo, tomadas de las guías de la Semana 5 y la Semana 6.
Unidad 3

CRUD Dinámico y Sesiones

Pregunta 01 oficial · lista de cotejo 3.4

¿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.

class Metamodel + fieldscampos HTML generadosvalidación del modelovista + plantilla + ruta a mano
Mínimo para correcta: decir que genera campos y validaciones a partir del modelo + al menos 2 cosas que siguen siendo manuales (vista, plantilla, ruta, propietario o reglas propias).
En tu código — core/forms.py
# 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']
Pregunta 02 oficial · lista de cotejo 3.4

¿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.

token ocultoCSRF = petición falsificada entre sitiosCsrfViewMiddleware403 Forbiddensolo en POST
Mínimo para correcta: explicar que protege contra envíos desde otro sitio + que sin él la respuesta es 403.
En tu código — core/templates/core/servicio_form.html
{# 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 %}
Pregunta 03 oficial · lista de cotejo 3.4

¿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.

un solo sentidoPBKDF2 / SHA-256sal aleatoriano se puede desencriptarse compara el hash
Mínimo para correcta: decir que es irreversible + que el login compara hashes (no “descifra”). Mencionar la sal o el algoritmo suma, pero no es obligatorio.
En tu práctica — /admin → Usuarios → usuario_a → campo Contraseña
# así se ve lo que guarda MySQL en auth_user.password (nunca la contraseña real)
pbkdf2_sha256$870000$Xy3k...sal...$Qm9v...hash...=
Pregunta 04 oficial · lista de cotejo 3.4

¿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.

all = todos los registrosfilter = WHEREpropietario_idrequest.useraislamiento de datos
Mínimo para correcta: decir que filter limita a los del usuario actual (WHERE / propietario) y que request.user es el usuario de la sesión.
En tu código — core/views.py, vista servicios
# filter(propietario=request.user): SELECT ... WHERE propietario_id = <id del usuario logueado>
lista_servicios = Servicio.objects.filter(propietario=request.user)
Pregunta 05 oficial · lista de cotejo 3.4

¿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.

cambiar el id en la URLregistro ajenopk + propietario404 Not Foundeditar y eliminar
Mínimo para correcta: definir IDOR con el ejemplo de cambiar el id en la URL + explicar que la segunda condición (propietario) provoca el 404.
En tu código — core/views.py, editar_servicio y eliminar_servicio
# 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:

  1. Inicias sesión como usuario_a y editas tu servicio: la dirección es /servicios/7/editar/.
  2. Ese 7 es el id (la llave primaria, pk) del registro en la tabla core_servicio. Los ids son números seguidos: 7, 8, 9…
  3. En la barra del navegador cambias el 7 por un 8. Nadie te lo impide: la URL la escribe el usuario.
  4. Si la vista solo busca por pk, Django encuentra el servicio 8 aunque sea de usuario_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)».

Pregunta 06

¿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.

no lo decide el usuariolo asigna la vistarequest.userno se puede falsificar
Mínimo para correcta: decir que se asigna en la vista con request.user + por qué (evitar que el usuario elija o falsifique el dueño).
Pregunta 07

¿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.

objeto en memoriasin INSERT todavíacompletar propietarioluego save()
Mínimo para correcta: decir que no guarda aún + que sirve para completar el propietario antes del save().
En tu código — core/views.py, crear_servicio
# 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()
Pregunta 08

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.

request.methodGET = mostrarPOST = enviaris_valid()errores en la misma página
Mínimo para correcta: distinguir qué pasa en GET y en POST + mencionar is_valid().

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).

1 · GET: abres /servicios/nuevo/→ formulario vacío→ 2 · POST: clic en «Guardar»→ is_valid()→ ✓ válido: save() + redirect ✗ inválido: misma página con errores

Paso a paso de las dos visitas a la misma URL:

  1. Primera visita (GET): escribes la dirección o das clic en «+ Nuevo servicio». El navegador hace un GET. La vista entra al else, crea ServicioForm() vacío y lo dibuja. Al editar se usa ServicioForm(instance=servicio), que llega precargado con los datos actuales.
  2. Segunda visita (POST): llenas los campos y das «Guardar». El <form method="POST"> manda los datos a la misma URL. La vista entra al if request.method == 'POST' y reconstruye el formulario con ServicioForm(request.POST).
  3. Validación: is_valid() revisa campos obligatorios, tipos, max_length y tu clean_precio.
  4. 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).
  5. Si no es válido: no guarda nada, y el render del 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.
En tu código — core/views.py, crear_servicio (las dos ramas)
# 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).

Pregunta 09

¿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.

Post/Redirect/Getevita duplicadosrecargar F5redirect = nuevo GET
Mínimo para correcta: decir que evita que al recargar se reenvíe el formulario (registros duplicados).
Pregunta 10

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.

mismo registroprecarga valoresUPDATE, no INSERTsin instance = duplicado
Mínimo para correcta: mencionar que precarga los datos + que save() actualiza en vez de crear.
En tu código — core/views.py, editar_servicio
# instance=servicio: el formulario actualiza ESE registro en vez de crear uno nuevo
form = ServicioForm(request.POST, instance=servicio)
Pregunta 11

¿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 %}.

GET no modifica datosconfirmaciónPOST + csrf_tokenborrado accidental
Mínimo para correcta: explicar que un GET puede dispararse sin querer + que el POST de confirmación lleva CSRF.
Pregunta 12

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_<campo>lo llama is_valid()cleaned_dataValidationErrorreturn del valor
Mínimo para correcta: decir que lo llama is_valid() por el nombre clean_campo + qué hace ValidationError.
En tu código — core/forms.py
# 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
Pregunta 13

¿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.

decoradoris_authenticatedredirige a LOGIN_URL?next=no ejecuta la vista
Mínimo para correcta: decir que bloquea la vista sin sesión + que redirige a LOGIN_URL.
En tu código — miproyecto/settings.py
# LOGIN_URL: a dónde manda @login_required a un visitante sin sesión
LOGIN_URL = 'login'
Pregunta 14

¿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().

registro vs logincrea el Userverifica credencialesget_user()contraseña hasheada
Mínimo para correcta: asignar bien cuál registra y cuál inicia sesión + una cosa concreta que hace cada uno.
Pregunta 15

¿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.

sesión en el servidorcookie sessioniddjango_sessionrequest.userAnonymousUser
Mínimo para correcta: explicar que login guarda la sesión y la liga a una cookie + que logout la borra.
Pregunta 16

¿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.

messages.success / infosiguiente páginafor message in messagesse borran al mostrarse
Mínimo para correcta: decir dónde se crean (vista) y dónde se muestran (plantilla) + que desaparecen tras mostrarse.
Pregunta 17

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 %}.

{% if user.is_authenticated %}context processoruser = request.userAnonymousUser
Mínimo para correcta: mencionar la condición is_authenticated + que user está disponible en la plantilla sin pasarlo en la vista.
En tu código — core/templates/core/base.html
{# 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>
Pregunta 18

¿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).

plantilla base / molde{% block %}{% extends %} primerono repetir código
Mínimo para correcta: explicar molde + bloques que rellenan las hijas + una ventaja.
Pregunta 19

¿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.

convertidor intargumento pk de la vistaname= de la rutareverse de URL
Mínimo para correcta: decir que captura el id y lo pasa a la vista + que {% url %} arma la dirección con el name y el pk.
En tu código — core/urls.py y servicios.html
# <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'),
Pregunta 20

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.

DEBUG oculta detallesSECRET_KEY firma sesionesALLOWED_HOSTS dominio exactoCSRF_TRUSTED_ORIGINS con httpsvariables de entorno
Mínimo para correcta: explicar correctamente al menos 3 de las 4 variables + que viven en variables de entorno de Render, no en el código.
↑ Números