Módulo 5 · Lección 16 de 30
ViewSet y enrutadores
Una clase por recurso, las acciones que no caben en los cinco verbos, y el dinero que vuelve a ser un número en cuanto te saltas el serializador.
Un conjunto de vistas agrupa las cinco operaciones de un recurso en una clase, y un enrutador genera las rutas a partir de ella. Es lo normal en DRF y es lo que conviene usar.
Lo interesante no es eso, que son dos líneas. Es lo que pasa cuando el recurso tiene acciones que no son ninguna de las cinco, que es la pregunta que la lección 11 dejó abierta.
Lo básico#
class LibroViewSet(viewsets.ModelViewSet):
queryset = Libro.objects.select_related('editorial').prefetch_related('autores')
serializer_class = LibroSerializerrouter = DefaultRouter()
router.register('libros', LibroViewSet)
urlpatterns = [path('api/', include(router.urls))]La diferencia con las genéricas de la lección anterior no es de potencia, es de organización: en vez de dos clases y dos rutas escritas a mano, hay una clase y el enrutador deduce el resto. Con doce recursos, eso son doce líneas en lugar de cuarenta y ocho.
Y hay tres tipos que conviene distinguir:
| Clase | Qué trae |
|---|---|
ViewSet | Nada. Escribes list, retrieve, etc. a mano |
GenericViewSet | El conjunto de datos y el serializador, sin acciones |
ModelViewSet | Las cinco acciones |
ReadOnlyModelViewSet | Listar y consultar. Nada más |
ReadOnlyModelViewSet es la más infravalorada. Para un catálogo público o un listado de países, es una palabra en lugar de configurar permisos que impidan escribir. Y lo que no existe no se puede romper.
Con GenericViewSet y las piezas de la lección anterior se compone lo que quieras:
class LibroViewSet(mixins.ListModelMixin,
mixins.RetrieveModelMixin,
mixins.CreateModelMixin,
viewsets.GenericViewSet):
"""Se puede crear y consultar, pero no modificar ni borrar."""Eso da un recurso donde no existe el borrado, y por tanto no hay permiso que se pueda configurar mal. Es mejor defensa que un permiso bien puesto.
Las acciones propias#
Aquí está la respuesta a la pregunta de la lección 11. Un decorador:
from rest_framework.decorators import action
class LibroViewSet(viewsets.ModelViewSet):
queryset = Libro.objects.select_related('editorial')
serializer_class = LibroSerializer
@action(detail=True, methods=['post'])
def publicar(self, peticion, pk=None):
libro = self.get_object()
if libro.estado == Libro.Estado.PUBLICADO:
return Response({'detail': 'Ya estaba publicado.'},
status=status.HTTP_409_CONFLICT)
libro.estado = Libro.Estado.PUBLICADO
libro.publicado_en = timezone.now().date()
libro.save(update_fields=['estado', 'publicado_en'])
return Response(self.get_serializer(libro).data)
@action(detail=False, url_path='mas-caros')
def mas_caros(self, peticion):
consulta = self.filter_queryset(self.get_queryset()).order_by('-precio')[:5]
return Response(self.get_serializer(consulta, many=True).data)Funcionando:
estado antes: borrador
POST /api/libros/2/publicar/ -> 200, estado: publicado
POST /api/libros/2/publicar/ -> 409 {"detail": "Ya estaba publicado."}
GET /api/libros/mas-caros/ -> 200 ['44.79', '44.63', '44.33', '44.12', '44.10']
GET /api/libros/2/publicar/ -> 405 {"detail": "Método \"GET\" no permitido."}El 409 es la decisión de diseño que merece la pena: publicar dos veces no es un error del cliente ni un éxito, es un conflicto con el estado actual, y es el código que la lección 2 reservaba para eso.
detail=True o detail=False es lo único que hay que decidir:
| Ruta | Qué recibe | |
|---|---|---|
detail=True | /libros/42/publicar/ | Un objeto, con self.get_object() |
detail=False | /libros/mas-caros/ | La colección, con self.get_queryset() |
Y self.get_object() no es Libro.objects.get(pk=pk): pasa por get_queryset y por los permisos de objeto de la lección 18. Usar el atajo se salta las dos cosas, que es un agujero de seguridad con forma de comodidad.
Lo que genera el enrutador#
^libros/$ name=libro-list
^libros/mas-caros/$ name=libro-top
^libros/novedades/$ name=libro-novedades
^libros/(?P<pk>[^/.]+)/$ name=libro-detail
^libros/(?P<pk>[^/.]+)/publicacion/$ name=libro-publicacion
^libros/(?P<pk>[^/.]+)/publicar/$ name=libro-publicarFíjate en el orden: las acciones de colección van antes que la ruta de detalle. Es deliberado y necesario, porque /libros/mas-caros/ encajaría con /libros/<pk>/ y mas-caros acabaría interpretado como identificador. El enrutador lo resuelve, y es la clase de detalle que se agradece cuando se entiende por qué está.
Los nombres se personalizan:
@action(detail=False, url_path='mas-caros', url_name='top')
def mas_caros(self, peticion):
...url_path es lo que se ve en la dirección y url_name lo que usas con reverse. Por defecto los dos salen del nombre del método con los guiones bajos convertidos, que produce /libros/mas_caros/ con guion bajo: feo en una dirección, y por eso conviene poner url_path a mano en cuanto el nombre lleva más de una palabra.
Serializador y permisos por acción#
Es lo que hace que un conjunto de vistas aguante un recurso real:
class LibroViewSet(viewsets.ModelViewSet):
queryset = Libro.objects.select_related('editorial').prefetch_related('autores')
def get_serializer_class(self):
if self.action == 'list':
return LibroListaSerializer
if self.action in ('create', 'update', 'partial_update'):
return LibroEscrituraSerializer
if self.action == 'estadisticas':
return EstadisticasSerializer
return LibroDetalleSerializer
def get_permissions(self):
if self.action in ('list', 'retrieve'):
return [permissions.AllowAny()]
if self.action == 'publicar':
return [EsEditor()]
return [permissions.IsAdminUser()]self.action vale 'list', 'retrieve', 'create', 'update', 'partial_update', 'destroy' o el nombre de tu acción propia. Eso último es lo que permite que publicar tenga sus propios permisos sin tocar nada más.
Dos cuidados:
get_permissions devuelve instancias, no clases. Con los paréntesis. Olvidarlos da un error confuso a la hora de comprobar, no al arrancar.
El return final es el que protege. Si la cadena de condicionales acaba en algo permisivo, cualquier acción nueva que añadas quedará abierta. Que acabe en lo restrictivo es la misma idea de "cerrado por defecto" de la lección 12, dentro de la clase.