Módulo 4 · Lección 22 de 45
Vistas basadas en clases
La otra forma de escribir vistas: un método por método HTTP, herencia en vez de copiar, y el precio de no ver el código que se ejecuta.
Todas las vistas que llevas son funciones, y funcionan. Pero en la lección 16 apareció una estructura que se va a repetir en cada vista que modifique datos:
def nuevo(peticion):
if peticion.method == 'POST':
...
return render(...)Ese if es siempre el mismo, y lo que hay a cada lado es casi siempre lo mismo también. Cuando una estructura se repite idéntica en veinte sitios, la respuesta del módulo 1 fue sacarla a una clase base y heredar. Eso es exactamente lo que hace la otra mitad de Django.
Una vista que es una clase#
from django.http import HttpResponse
from django.views import View
class Saluda(View):
def get(self, peticion):
return HttpResponse('GET desde una clase')
def post(self, peticion):
return HttpResponse('POST desde una clase')path('saluda/', views_clase.Saluda.as_view(), name='saluda'),El if peticion.method ha desaparecido. Cada método HTTP es un método de la clase, con su nombre en minúsculas, y Django llama al que toca.
Lo que hay en el fichero de rutas no es la clase, es Saluda.as_view(). Ese método devuelve una función, porque Django solo sabe llamar funciones. Esa función, cuando llega una petición, crea una instancia nueva de la clase y le pasa el control.
Eso último tiene una consecuencia práctica que evita un error desagradable: la instancia dura lo que dura la petición y no se comparte con nadie. Guardar algo en self es seguro. Guardarlo en un atributo de clase, no, porque eso sí se comparte entre peticiones.
Lo que sale gratis#
GET -> 200
POST -> 200
PUT -> 405 Allow: GET, POST, HEAD, OPTIONS
DELETE -> 405 Allow: GET, POST, HEAD, OPTIONS
HEAD -> 200
OPTIONS -> 200 Allow: GET, POST, HEAD, OPTIONSEsa tabla es buena parte del argumento a favor de las clases, y sale sin escribir nada.
PUT y DELETE dan 405 con su cabecera Allow, que es exactamente lo que pedía la lección 2, y la clase sabe qué poner ahí porque sabe qué métodos tiene definidos. Con una función, el 405 hay que ponerlo a mano y la cabecera casi nadie la pone.
OPTIONS se contesta solo, que es lo que preguntan los navegadores antes de una petición desde otro dominio.
Y HEAD funciona porque lo atiende el get: mismas cabeceras, sin cuerpo, tal como manda la especificación.
TemplateView#
Lo más habitual es una página que solo pinta una plantilla. Para eso hay una clase hecha:
from django.views.generic import TemplateView
class Portada(TemplateView):
template_name = 'contactos/portada.html'
def get_context_data(self, **kwargs):
contexto = super().get_context_data(**kwargs)
contexto['total'] = Contacto.objects.count()
return contextototal=5No hay get, no hay render, no hay return. Solo dos cosas: qué plantilla y qué va en el contexto.
El super().get_context_data(**kwargs) de la primera línea no es opcional. Lo que devuelve trae cosas que otras piezas esperan encontrar, entre ellas view, que es la propia vista:
view=<contactos.views_clase.Portada object at 0x10bb16510>Saltarse esa llamada y devolver un diccionario propio funciona hasta que algo de más arriba deja de encontrar lo suyo, y entonces el fallo es raro y lejano. Se llama al padre, se añade lo tuyo, se devuelve. Ese patrón de tres líneas se repite en todo el bloque.
Que view esté en el contexto tiene un efecto secundario cómodo: desde la plantilla se puede llamar a cualquier método de la vista con {{ view.lo_que_sea }}.
La jerarquía#
Aquí está lo que hay que entender de verdad, porque es donde las vistas de clase se vuelven opacas. Esto es lo que hay detrás de ListView:
ListView
MultipleObjectTemplateResponseMixin
TemplateResponseMixin
BaseListView
MultipleObjectMixin
ContextMixin
View
objectSiete clases. Ese es el precio: el código que se ejecuta no está donde escribes, está repartido en una cadena de clases que no has abierto nunca.
El reparto, que una vez visto se reconoce en todas:
| Trozo del nombre | De qué se ocupa |
|---|---|
View | Recibir la petición y llamar al método del verbo |
...Mixin | Una pieza suelta: obtener los objetos, el contexto, el formulario |
Base... | La lógica sin decidir cómo se responde |
TemplateResponseMixin | Responder con una plantilla |
| El nombre completo | Juntarlo todo |
Los nombres se leen: MultipleObjectMixin sabe conseguir una lista de objetos; TemplateResponseMixin sabe pintar una plantilla; ListView es las dos cosas a la vez. Por eso existen las parejas BaseListView y ListView: la primera sirve para responder con JSON, por ejemplo.
Y una que importa por lo que permite:
http_method_names: ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace']Recortar esa lista en una clase limita los métodos que acepta, que es la versión de clase del require_POST de la lección 16.
Función o clase#
Las dos formas conviven en el mismo proyecto sin problema, así que no hay que elegir de una vez.
Una función se lee entera. Todo lo que pasa está delante. Para algo que no se parece a nada, para una vista con lógica rara, para un endpoint suelto, la función gana y no hay discusión.
Una clase se configura. Cuando la vista es una de las cuatro o cinco cosas de siempre —listar, ver, crear, editar, borrar—, la clase son tres líneas y la función son veinte, y esas veinte están copiadas de otra vista.
El criterio que funciona:
| Situación | Qué elegir |
|---|---|
| Listar, ver, crear, editar o borrar, sin nada raro | Clase genérica |
| Lo mismo con un par de retoques | Clase, sobrescribiendo un método |
| Lógica que no se parece a un CRUD | Función |
| Estás aprendiendo qué hace Django | Función |
| Sobrescribes cuatro métodos y ya no sabes qué corre | Función, otra vez |
Esa última fila es importante y a mucha gente le cuesta llegar a ella. Una vista genérica con seis métodos sobrescritos es peor que la función equivalente, porque tiene la longitud de la función y encima hay que saberse la jerarquía para leerla. Cuando llegues ahí, reescribirla como función no es retroceder.