Usando ViewSets y Routers en Django REST Framework
Volviendo a la saga de Django REST Framework (Introducción a Django REST Framework y Permisos y autentificación con Django REST Framework), y retomando el ejemplo de las encuestas que usamos en dichos tutoriales, hoy vamos a ver como utlizar los ViewSets y Routers para simplificar todavía más nuestro código.
ViewSets
Django REST Framework incorpora una abstracción para trabajar con ViewSets, que nos permite concentrarnos en modelar el estado y las interacciones de la API, y dejar que la construcción de URLS se gestione automáticamente, en base a unas convenciones comunes.
Los ViewSet son clases similares a las clases View, con la diferencia de que en lugar de proporcionar métodos de gestión como get y put, proporciona operaciones de read y update.
Una clase ViewSet solo se vincula a un conjunto de métodos en el último momento, cuando se instancia en un conjunto de vistas, típicamente mediante una clase Router que gestiona la definición de URL conf. por nosotros.
Traduciendo nuestras vistas en ViewSets
Vamos a crear un ViewSet para nuestas vistas SurveyList y SurveyDetails, que recuerdo a continuación (en survey/views.py):
class SurveyMixin(object):
queryset = Survey.objects.all()
serializer_class = SurveySerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly, IsOwnerOrReadOnly)
def pre_save(self, obj):
print 'Entering PRE_SAVE method'
obj.owner = self.request.user
class SurveyList(SurveyMixin, ListCreateAPIView):
pass
class SurveyDetails(SurveyMixin, RetrieveUpdateDestroyAPIView):
pass
Pues bien, podemos eliminar dicho código, y reemplazarlo por este otro:
from rest_framework import viewsets
class SurveyViewSet(viewsets.ModelViewSet):
"""
This viewset automatically provides `list`, `create`, `retrieve`,
`update` and `destroy` actions.
Additionally we also provide an extra `votes` action.
"""
queryset = Survey.objects.all()
serializer_class = SurveySerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly, IsOwnerOrReadOnly)
def pre_save(self, obj):
obj.owner = self.request.user
Podemos ver como mantenemos los elementos queryset, serializer_class y permission_classes, así como el método pre_save, pero en este caso, nuestra clase SurveyViewSet hereda de ModelViewSet, y eso basta para que automáticamente disponga de los métodos:
- List (sería equivalente a nuestro antiguo SurveyList)
- Create
- Retrieve (sería equivalente a nuestro antiguo SurveyDetail)
- Update
- Destroy
Personalizando nuestra API
Además, vamos a incorporar una nueva funcionalidad: Queremos que al recuperar una encuesta, nos aparezcan todos sus votos. Atención al decorador @link y el método votes que incorporamos a nuestra clase:
from rest_framework import viewsets
from rest_framework.decorators import link
class SurveyViewSet(viewsets.ModelViewSet):
"""
This viewset automatically provides `list`, `create`, `retrieve`,
`update` and `destroy` actions.
Additionally we also provide an extra `votes` action.
"""
queryset = Survey.objects.all()
serializer_class = SurveySerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly, IsOwnerOrReadOnly)
'''si quisieramos una respuesta de texto plano
@link(renderer_classes=[renderers.StaticHTMLRenderer])
'''
@link()
def votes(self, request, *args, **kwargs):
survey = self.get_object()
votes = survey.survey_votes.all()
serializer = SurveyVotesSerializer(votes)
return Response(serializer.data)
def pre_save(self, obj):
obj.owner = self.request.user
Como podemos comprobar, básicamente lo que se hace al llamar al método es recuperar todos los votos de dicha encuesta, y devolverlos en json o el formato exigido a través del serializer de los votos.
El decorador @link nos permite crear una acción personalizada que no encaja con los métodos estandar create/update/delete. Dichas acciones personalizadas responderán a peticiones GET. Si lo que queremos es responder a peticiones POST, utilizaríamos en su lugar el decorador @action.
Vinculando URLs a ViewSets
Los métodos de handler solo se asocian a las acciones cuando definimos el URLConf. Para ver como funciona en detalle, antes de pasar a los Routers vamos a crear explícitamente un conjunto de vistas desde nuestro ViewSet.
Abrimos el archivo survey/urls.py, y añadimos lo siguiente para vincular nuestro ViewSet a vistas concretas:
survey_list = views.SurveyViewSet.as_view({
'get': 'list',
'post': 'create'
})
survey_detail = views.SurveyViewSet.as_view({
'get': 'retrieve',
'put': 'update',
'patch': 'partial_update',
'delete': 'destroy'
})
survey_votes = views.SurveyViewSet.as_view({
'get': 'votes'
})
'''esto si queremos una respuesta de texto plano
survey_votes = views.SurveyViewSet.as_view({
"get": "votes"
}, renderer_classes=[renderers.StaticHTMLRenderer])
'''
urlpatterns = patterns('api.views',
url(r'^$', views.api_root, name='api-root'),
url(r'^surveys/$', survey_list, name='survey-list'),
url(r'^survey/(?P<pk>[0-9]+)$', survey_detail, name='survey-detail'),
url(r'^survey/(?P<pk>[0-9]+)/votes/$', survey_votes, name='survey_votes'),
'''y continuamos con el resto de nuestras urls'''
)
urlpatterns = format_suffix_patterns(urlpatterns)
Como podemos ver, estamos creando 3 vistas concretas, a partir del mismo ViewSet:
- survey_list
- Vinculamos el método get de la vista al método list del ViewSet
- Vinculamos el método post de la vista al método create del ViewSet
- survey_detail
- Vinculamos el método get de la vista al método retrieve del ViewSet
- Vinculamos el método put de la vista al método update del ViewSet
- Vinculamos el método patch de la vista al método partial_update del ViewSet
- Vinculamos el método delete de la vista al método destroy del ViewSet
- survey_votes
- Vinculamos el método get de la vista al método personalizado votes del ViewSet
Posteriormente, enlazamos las urls que usábamos anteriormente para la lista y el detalle de las encuestas a las nuevas vistas, y creamos una nueva url para los votos.
Si ejecutamos el servidor de desarrollo y miramos los resultados, veremos que el funcionamiento para las vistas de lista y detalle de encuestas sigue siendo el mismo.
Pero además, ahora disponemos de una opción para obtener todos los votos relacionados con esta encuesta en concreto:
Routers
Dado que ahora usamos clases ViewSet, no necesitamos diseñar la configuración de URLs nosotros mismos. Las convenciones para vincular recursos con vistas y urls puede gestionarse automáticamente mediante una clase Router. Eso sí, perderemos la flexibilidad que nos ofrece nuestro propio diseño, y pasaremos a regirnos por las normas predefinidas de la clase Router.
Veamos como quedaría nuestra configuración de URLs usando un router para nuestro ViewSet survey/urls.py:
from django.conf.urls import patterns, include, url
from rest_framework import renderers
from rest_framework.routers import DefaultRouter
from rest_framework.urlpatterns import format_suffix_patterns
from . import views
# Creamos un router y registramos nuestros viewsets, en este caso solo uno
router = DefaultRouter()
router.register(r'surveys', views.SurveyViewSet)
urlpatterns = patterns('api.views',
#esto ya se encarga de la vista root, asi como de la lista y detalle de encuestas, y la opción personalizada de votos
url(r'^', include(router.urls)),
'''Mantenemos el resto de nuestras URLs
...
'''
)
#El Router incorpora automáticamente los sufijos, por lo que de no comentar esta línea tendríamos problemas.
#urlpatterns = format_suffix_patterns(urlpatterns)
Eso sí, ahora veremos que las URLS que dependen del router siguen el siguiente patrón:
- http://127.0.0.1:8000/api/surveys/
- http://127.0.0.1:8000/api/surveys/1/
- http://127.0.0.1:8000/api/surveys/1/votes/
Es decir, como las genera de forma automática, no tiene en cuenta el paso de plural a singular que hacíamos nosotros para consultar el detalle de una encuesta. No obstante, es un mal menor para lo simplificado que queda nuestro código.
En todo caso, es cosa de cada uno decidir hasta que nivel quiere recaer en las capas de abstracción que proporciona Django REST Framework y a partir de qué momento quiere tener un control más directo del código. Como la mayoría de las veces, aquí no hay una única respuesta correcta.
¡Saludos!