Saltar al contenido
🐍 Curso de Python
/

Módulo 4 · Lección 16 de 45

Vistas basadas en funciones

Juntar por fin los modelos con las páginas, dejar de construir HTML con f-strings, y la carpeta que se repite dos veces con motivo.

⏱ 15 min de lectura

En la lección 6 escribiste una vista que devolvía HTML con una f-string, y quedó dicho dos veces que eso era provisional: una porque los datos estaban a mano, y otra porque montar HTML concatenando cadenas es el agujero de seguridad de la lección 1. El bloque B resolvió lo primero. Esta lección resuelve lo segundo y cierra el circuito: petición, vista, modelo, plantilla, respuesta.

render, que hace tres cosas#

contactos/views.py
from django.shortcuts import get_object_or_404, render

from .models import Contacto


def lista(peticion):
        contactos = Contacto.objects.select_related('grupo')
    return render(peticion, 'contactos/lista.html', {'contactos': contactos})

render recibe tres cosas y hace tres cosas:

  1. La petición, que le hace falta para que la plantilla sepa quién está mirando y para la protección de los formularios.
  2. El nombre de la plantilla, que se busca en las carpetas de todas las aplicaciones instaladas.
  3. El contexto, un diccionario con lo que la plantilla puede usar. Las claves son los nombres que aparecerán dentro.

Con eso rellena la plantilla y devuelve un HttpResponse ya construido. Es una comodidad sobre tres pasos que se pueden hacer a mano y que casi nadie hace.

Fíjate en el select_related('grupo'). Es la deuda que dejó la lección 13: la plantilla va a recorrer los contactos y a pedir el grupo de cada uno, así que sin eso habría una consulta por contacto. Esa es la regla práctica del N+1: si la plantilla recorre una lista y toca una relación, la vista que la alimenta lleva select_related.

Dónde van las plantillas#

text
contactos/
  templates/
    contactos/
      lista.html
      detalle.html

La carpeta contactos aparece dos veces y es intencionado. La primera, templates/, es donde Django busca; la segunda es un espacio de nombres para que las plantillas de una aplicación no choquen con las de otra.

El motivo se ve provocándolo. Con dos aplicaciones que tengan cada una un templates/lista.html sin la carpeta de en medio:

Salida en pantalla
SOY LA DE CONTACTOS

Gana la que aparezca antes en INSTALLED_APPS, y la otra plantilla deja de existir sin que nadie avise. No hay error, no hay aviso: simplemente sale la página equivocada. Con la carpeta intermedia, una es contactos/lista.html y la otra pedidos/lista.html, y no hay forma de confundirlas.

Es una convención fea que se agradece en cuanto un proyecto tiene tres aplicaciones. Conviene adoptarla desde la primera.

La plantilla#

contactos/templates/contactos/lista.html
<!doctype html>
<html lang="es">
<body>
  <h1>Contactos ({{ contactos|length }})</h1>
  <ul>
  {% for contacto in contactos %}
    <li>{{ contacto.nombre }} — {{ contacto.grupo|default:"sin grupo" }}</li>
  {% empty %}
    <li>No hay contactos.</li>
  {% endfor %}
  </ul>
</body>
</html>
Salida en pantalla
<h1>Contactos (5)</h1>
<ul>
  <li>Ana Ruiz — Trabajo</li>
  <li>Luis Pardo — Familia</li>
  <li>Sin grupo — sin grupo</li>
  <li>Zoe Alvarez — Trabajo</li>
</ul>

El lenguaje de plantillas es la lección siguiente entera. Lo que hace falta ver ahora es lo que se ha arreglado sin pedirlo:

consola
>>> c.notas = '<script>alert(1)</script> y <b>negrita</b>'
>>> c.save()
Salida en pantalla
<p>&lt;script&gt;alert(1)&lt;/script&gt; y &lt;b&gt;negrita&lt;/b&gt;</p>

El <script> ha salido como texto, no como código. Las plantillas escapan todo lo que insertan, por defecto y sin hacer nada. Eso es lo que la f-string de la lección 6 no hacía, y es la razón de peso para usar plantillas incluso cuando el HTML es de tres líneas.

Una vista de detalle#

contactos/views.py
def detalle(peticion, pk):
        contacto = get_object_or_404(Contacto, pk=pk)
    return render(peticion, 'contactos/detalle.html', {'contacto': contacto})
contactos/urls.py
urlpatterns = [
    path('', views.lista, name='lista'),
    path('<int:pk>/', views.detalle, name='detalle'),
]

Ese <int:pk> captura un trozo de la dirección y se lo pasa a la vista como argumento con ese nombre. Por eso la función tiene un parámetro pk: /contactos/1/ llama a detalle(peticion, pk=1). Los conversores y las rutas con nombre son la lección 21; aquí basta con el más común.

El int no es decorativo:

Salida en pantalla
/contactos/1/     -> 200
/contactos/999/   -> 404
/contactos/abc/   -> 404

Los dos últimos dan 404 por motivos distintos. El 999 encaja con el patrón, llega a la vista y get_object_or_404 no encuentra la fila. El abc ni siquiera encaja con el patrón, porque int solo acepta dígitos, así que la vista no llega a ejecutarse. Eso es una ventaja: los datos que llegan a la vista ya tienen el tipo correcto y no hay que comprobarlo.

get_object_or_404 es el atajo que se anunció en la lección 11. Hace un get y convierte el DoesNotExist en la respuesta correcta:

python
contacto = get_object_or_404(Contacto, pk=pk)                     # por clave
contacto = get_object_or_404(Contacto, correo='ana@ejemplo.es')   # por otro campo único

Hay una pareja, get_list_or_404, que hace lo mismo con un filter y da 404 si la lista sale vacía. Se usa mucho menos, porque una lista vacía casi siempre es una página válida que dice "no hay nada".

Atender GET y POST#

Una vista recibe los dos métodos y tiene que decidir. La estructura es siempre esta:

contactos/views.py
from django.shortcuts import redirect


def nuevo(peticion):
        if peticion.method == 'POST':
                Contacto.objects.create(
            nombre=peticion.POST['nombre'],
            correo=peticion.POST['correo'],
        )
        return redirect('contactos:lista')
    return render(peticion, 'contactos/nuevo.html', {})

Ahí está el patrón de la lección 2: se guarda y se redirige, no se contesta con la página. redirect construye el 302 con su cabecera Location, y admite el nombre de una ruta, una dirección o un objeto con get_absolute_url.

Para las vistas que solo admiten un método hay un decorador que ahorra el if:

python
from django.views.decorators.http import require_POST


@require_POST
def borrar(peticion, pk):
        get_object_or_404(Contacto, pk=pk).delete()
    return redirect('contactos:lista')

Con eso, un GET a esa dirección devuelve un 405, que es el código correcto según la lección 2. Y es justo la protección que hacía falta en la historia del buscador que vació la base de datos.

Los tipos de respuesta#

DevolverPara qué
render(...)Una página HTML. Lo normal
redirect('contactos:lista')Mandar a otra dirección
HttpResponse('texto')Texto suelto, o contenido que no es HTML
JsonResponse({'clave': 'valor'})Datos en JSON, para JavaScript
HttpResponse(status=204)Ha ido bien y no hay nada que enseñar
raise Http404('...')Como get_object_or_404, pero decidiéndolo tú
FileResponse(open(...), as_attachment=True)Una descarga

JsonResponse merece una mención: convierte el diccionario y pone el Content-Type: application/json. Con eso ya se puede hacer una API pequeña, y de hecho así se empieza el módulo 5 antes de llegar a Django REST Framework.