Banco de Preguntas Conceptuales — Unidad 2
Las 20 preguntas del “Examen conceptual rápido” de la Unidad 2 (Django, arquitectura MTV, MySQL en Aiven, ORM y migraciones, despliegue como Web Service en Render; Semanas 3-4). 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 2.
Django + MySQL en la nube + Render
Recorre el flujo completo de una petición: desde que el navegador pide una URL hasta que Django devuelve el HTML final.
Respuesta modelo: El navegador pide /servicios/ → el urls.py del proyecto la delega con include al urls.py de core → esa ruta llama a la función servicios de views.py → la vista consulta el modelo (Servicio.objects.all()) y llama a render con una plantilla → Django rellena servicios.html y devuelve el HTML como respuesta HTTP.
# urls.py: la dirección 'servicios/' apunta a la función views.servicios
path('servicios/', views.servicios, name='servicios'),
# views.py: la función junta datos + plantilla y devuelve la respuesta
return render(request, 'core/servicios.html', {'servicios': lista_servicios})¿Qué responsabilidad tiene el Model, cuál la View y cuál el Template? Da un ejemplo con tu propio Servicio.
Respuesta modelo: Model describe los datos y su tabla (class Servicio en models.py); View es la función que recibe la petición, pide datos al modelo y elige la plantilla (def servicios); Template es el HTML con {{ }} y {% %} que muestra esos datos (servicios.html).
¿Qué ventaja tiene escribir Servicio.objects.all() en vez de un SELECT * FROM core_servicio a mano?
Respuesta modelo: El ORM traduce Python a SQL por ti: escribes Python, no dependes de la sintaxis de un motor concreto, obtienes objetos (no filas sueltas) con sus campos como atributos, y Django escapa los valores, lo que protege contra inyección SQL.
# Python que escribes: Django lo convierte en SELECT ... FROM core_servicio
lista_servicios = Servicio.objects.all()¿Por qué makemigrations y migrate son dos comandos separados y no uno solo?
Respuesta modelo: makemigrations solo compara tus modelos con las migraciones existentes y escribe un archivo con el plan de cambios (ej. 0001_initial.py); no toca la base. migrate ejecuta ese plan contra MySQL. Separarlos permite revisar el plan, versionarlo en Git y aplicarlo igual en otro entorno (Render).
# genera core/migrations/0001_initial.py (solo el plan)
$ python manage.py makemigrations core
# ejecuta el plan: crea la tabla core_servicio en MySQL
$ python manage.py migrate¿Qué pasaría si subieras tu archivo .env real a un repositorio público de GitHub?
Respuesta modelo: Cualquiera podría leer el host, usuario y contraseña de tu MySQL en Aiven (y tu SECRET_KEY) y entrar a tu base de datos: ver, cambiar o borrar datos. Borrarlo después no alcanza porque queda en el historial de Git; hay que cambiar las credenciales. Por eso .env va en .gitignore y se sube solo .env.example.
# .env: el archivo con tus contraseñas reales, nunca debe llegar a GitHub
.env¿Qué es el entorno virtual (venv) y qué relación tiene con requirements.txt?
Respuesta modelo: El venv es una copia aislada de Python solo para este proyecto: lo que instalas con pip no se mezcla con otros proyectos. No se sube a GitHub; en su lugar se sube requirements.txt (hecho con pip freeze), la lista con la que cualquiera —incluido Render— recrea el mismo entorno con pip install -r requirements.txt.
¿Para qué instalaste pymysql y qué hace pymysql.install_as_MySQLdb()?
Respuesta modelo: Django necesita un driver para hablar con MySQL. PyMySQL es un conector escrito en Python puro; install_as_MySQLdb() hace que Django lo use como si fuera el driver oficial (MySQLdb), que es el que espera el backend django.db.backends.mysql.
# pymysql: el conector que instalaste con pip
import pymysql
# Django lo usa como si fuera el driver oficial MySQLdb
pymysql.install_as_MySQLdb()¿Qué diferencia hay entre .env y .env.example, y cuál entregas?
Respuesta modelo: .env tiene los valores reales (contraseña de Aiven, etc.), vive solo en tu computadora y está en .gitignore. .env.example tiene las mismas claves con valores de relleno, se sube al repo y documenta qué variables necesita el proyecto. Se entrega .env.example.
¿Qué hace config('DB_PORT', default='3306') y para qué sirven default y cast?
Respuesta modelo: config busca la variable primero en las variables de entorno del sistema (lo que usa Render) y si no, en el .env. default es el valor que se usa si la variable no existe. cast convierte el texto leído a otro tipo, por ejemplo cast=bool para DEBUG o cast=Csv() para ALLOWED_HOSTS.
# PORT: lee DB_PORT; si no está definida, usa '3306'
'PORT': config('DB_PORT', default='3306'),¿Por qué el bloque DATABASES tiene 'OPTIONS': {'ssl': {'ssl-mode': 'REQUIRED'}}?
Respuesta modelo: Porque la base está en la nube y la conexión viaja por internet: Aiven exige que vaya cifrada (SSL/TLS) para que nadie pueda leer el usuario, la contraseña ni los datos en el camino. Sin esa opción, la conexión es rechazada.
¿Qué es una “app” de Django y por qué la plantilla va en core/templates/core/ (con “core” repetido)?
Respuesta modelo: Una app es una unidad de funcionalidad dentro del proyecto (la creaste con startapp core y la registraste en INSTALLED_APPS). La carpeta repetida es una convención: Django junta las plantillas de todas las apps, y el subdirectorio con el nombre de la app evita que dos apps con un servicios.html se confundan; por eso se pide 'core/servicios.html'.
¿Qué diferencia hay entre {{ servicio.titulo }} y {% for servicio in servicios %} en una plantilla?
Respuesta modelo: {{ }} muestra un valor (una variable del contexto o un atributo). {% %} es una etiqueta que ejecuta lógica de plantilla (bucles, condiciones, url) y no imprime nada por sí misma. El for necesita cerrar con {% endfor %}.
<!-- {% for %}: repite la fila por cada servicio -->
{% for servicio in servicios %}
<!-- {{ }}: imprime el título del servicio actual -->
<td>{{ servicio.titulo }}</td>
<!-- cierra el bucle -->
{% endfor %}¿Para qué sirve include('core.urls') en el urls.py del proyecto y para qué el name= de cada ruta?
Respuesta modelo: include delega un grupo de direcciones a otra app: el proyecto dice “todo lo que llegue a la raíz lo resuelve core/urls.py”. name= es un alias interno para referirse a la ruta sin escribir la dirección, por ejemplo con {% url 'servicios' %}; si la dirección cambia, los enlaces siguen funcionando.
¿Por qué propietario es un ForeignKey a User y qué hace on_delete=models.CASCADE?
Respuesta modelo: Porque cada servicio pertenece a un usuario: la ForeignKey crea en MySQL la columna propietario_id, que apunta a una fila de la tabla de usuarios (relación muchos-a-uno: un usuario, muchos servicios). CASCADE indica que si se borra ese usuario, se borran también sus servicios.
# ForeignKey: genera la columna propietario_id, que apunta a un User
propietario = models.ForeignKey(User, on_delete=models.CASCADE)¿Qué pasa en /admin si tu modelo no tiene el método __str__?
Respuesta modelo: Cada registro aparecería como “Servicio object (1)”, “Servicio object (2)”… porque Django no sabría qué texto usar para representarlo. __str__ define ese texto; en tu modelo devuelve self.titulo. No requiere migración porque no cambia la tabla.
¿Por qué en Render se usa gunicorn y no python manage.py runserver?
Respuesta modelo: runserver es un servidor de desarrollo: pensado para una sola persona en su computadora, sin la seguridad ni el rendimiento necesarios. gunicorn es un servidor WSGI de producción que atiende varias peticiones a la vez; en Render se arranca con el Start Command gunicorn miproyecto.wsgi (el nombre de tu proyecto).
¿Qué problema resuelve WhiteNoise y qué hace collectstatic con STATIC_ROOT?
Respuesta modelo: Con DEBUG=False Django deja de servir los archivos estáticos (CSS/JS/imágenes, como los del admin). WhiteNoise permite que la propia app los sirva sin un servidor aparte. collectstatic (en el Build Command) copia todos los estáticos de las apps a la carpeta STATIC_ROOT (staticfiles/), de donde WhiteNoise los entrega comprimidos. Su middleware va justo después de SecurityMiddleware.
# STATIC_ROOT: carpeta donde collectstatic junta todos los estáticos
STATIC_ROOT = BASE_DIR / 'staticfiles'¿Por qué en Render DEBUG debe ser False y qué controla ALLOWED_HOSTS?
Respuesta modelo: Con DEBUG=True, ante un error Django muestra código, rutas y configuración a cualquier visitante. En producción va en False y se ve un error genérico. ALLOWED_HOSTS es la lista de dominios que Django acepta atender; con DEBUG=False, si el dominio de Render no está ahí, responde Bad Request (400).
¿Por qué en la Unidad 1 fue un Static Site y en la Unidad 2 un Web Service en Render? ¿Qué hacen el Build y el Start Command?
Respuesta modelo: El portafolio era HTML/CSS fijo: bastaba con servir archivos. Django necesita un proceso de Python corriendo todo el tiempo que atienda cada petición y hable con MySQL: eso es un Web Service. El Build Command prepara (pip install -r requirements.txt + collectstatic + migrate) y el Start Command arranca el servidor (gunicorn ...wsgi).
Tu computadora y Render usan la misma MySQL de Aiven, y migrate está en el Build Command de Render. Si ya migraste en local, ¿qué pasa en el siguiente deploy y por qué no se usa la Shell?
Respuesta modelo: En el plan Free de Render no existe la pestaña Shell, por eso migrate va al final del Build Command y corre solo en cada deploy. Al correr, Django revisa la tabla django_migrations de la base de Aiven, ve que todas las migraciones ya están aplicadas (las aplicaste desde tu compu contra esa misma base) y el log dice “No migrations to apply”. Si el push trae una migración nueva que nadie aplicó, ese mismo paso la aplica (Applying ... OK). Por eso los datos cargados en /admin de Render también se ven en local.
migrate corre en el Build Command (no en una Shell) + que la base es la misma y Django sabe qué aplicó (django_migrations / “No migrations to apply”).# instala paquetes, junta estáticos y aplica migraciones en Aiven, en cada deploy
pip install -r requirements.txt && python manage.py collectstatic --noinput && python manage.py migrate