From c785628300d2b7cce63862a18915c537f8a3ab24 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 4 Apr 2013 20:00:44 +0100 Subject: [PATCH] Fleshing out viewsets/routers --- docs/api-guide/viewsets-routers.md | 50 +++++++++++++- rest_framework/resources.py | 75 --------------------- rest_framework/routers.py | 43 ++++++++++++ rest_framework/viewsets.py | 105 ++++++++++++++++++++++------- 4 files changed, 171 insertions(+), 102 deletions(-) delete mode 100644 rest_framework/resources.py create mode 100644 rest_framework/routers.py diff --git a/docs/api-guide/viewsets-routers.md b/docs/api-guide/viewsets-routers.md index 817e1b8f7..7813c00d3 100644 --- a/docs/api-guide/viewsets-routers.md +++ b/docs/api-guide/viewsets-routers.md @@ -48,6 +48,14 @@ If we need to, we can bind this viewset into two seperate views, like so: Typically we wouldn't do this, but would instead register the viewset with a router, and allow the urlconf to be automatically generated. +There are two main advantages of using a `ViewSet` class over using a `View` class. + +* Repeated logic can be combined into a single class. In the above example, we only need to specify the `queryset` once, and it'll be used across multiple views. +* By using routers, we no longer need to deal with wiring up the URL conf ourselves. + +Both of these come with a trade-off. Using regular views and URL confs is more explicit and gives you more control. ViewSets are helpful if you want to get up and running quickly, or when you have a large API and you want to enforce a consistent URL configuration throughout. + + # API Reference ## ViewSet @@ -62,10 +70,50 @@ The `ModelViewSet` class inherits from `GenericAPIView` and includes implementat The actions provided by the `ModelViewSet` class are `.list()`, `.retrieve()`, `.create()`, `.update()`, and `.destroy()`. +#### Example + +Because `ModelViewSet` extends `GenericAPIView`, you'll normally need to provide at least the `queryset` and `serializer_class` attributes. For example: + + class AccountViewSet(viewsets.ModelViewSet): + """ + A simple ViewSet for viewing and editing accounts. + """ + queryset = Account.objects.all() + serializer_class = AccountSerializer + permission_classes = [IsAccountAdminOrReadOnly] + +Note that you can use any of the standard attributes or method overrides provided by `GenericAPIView`. For example, to use a `ViewSet` that dynamically determines the queryset it should operate on, you might do something like this: + + class AccountViewSet(viewsets.ModelViewSet): + """ + A simple ViewSet for viewing and editing the accounts + associated with the user. + """ + serializer_class = AccountSerializer + permission_classes = [IsAccountAdminOrReadOnly] + + def get_queryset(self): + return request.user.accounts.all() + +Also note that although this class provides the complete set of create/list/retrieve/update/destroy actions by default, you can restrict the available operations by using the standard permission classes. + ## ReadOnlyModelViewSet The `ReadOnlyModelViewSet` class also inherits from `GenericAPIView`. As with `ModelViewSet` it also includes implementations for various actions, but unlike `ModelViewSet` only provides the 'read-only' actions, `.list()` and `.retrieve()`. +#### Example + +As with `ModelViewSet`, you'll normally need to provide at least the `queryset` and `serializer_class` attributes. For example: + + class AccountViewSet(viewsets.ReadOnlyModelViewSet): + """ + A simple ViewSet for viewing accounts. + """ + queryset = Account.objects.all() + serializer_class = AccountSerializer + +Again, as with `ModelViewSet`, you can use any of the standard attributes and method overrides available to `GenericAPIView`. + # Custom ViewSet base classes Any standard `View` class can be turned into a `ViewSet` class by mixing in `ViewSetMixin`. You can use this to define your own base classes. @@ -90,7 +138,7 @@ For example, the definition of `ModelViewSet` looks like this: By creating your own base `ViewSet` classes, you can provide common behavior that can be reused in multiple views across your API. -Note the that `ViewSetMixin` class can also be applied to the standard Django `View` class if you want to use REST framework's automatic routing, but don't want to use it's permissions, authentication and other API policies. +For advanced usage, it's worth noting the that `ViewSetMixin` class can also be applied to the standard Django `View` class. Doing so allows you to use REST framework's automatic routing, but don't want to use it's permissions, authentication and other API policies. --- diff --git a/rest_framework/resources.py b/rest_framework/resources.py deleted file mode 100644 index d4019a94e..000000000 --- a/rest_framework/resources.py +++ /dev/null @@ -1,75 +0,0 @@ -##### RESOURCES AND ROUTERS ARE NOT YET IMPLEMENTED - PLACEHOLDER ONLY ##### - -from functools import update_wrapper -from django.utils.decorators import classonlymethod -from rest_framework import views, generics, mixins - - -##### RESOURCES AND ROUTERS ARE NOT YET IMPLEMENTED - PLACEHOLDER ONLY ##### - -class ResourceMixin(object): - """ - This is the magic. - - Overrides `.as_view()` so that it takes an `actions` keyword that performs - the binding of HTTP methods to actions on the Resource. - - For example, to create a concrete view binding the 'GET' and 'POST' methods - to the 'list' and 'create' actions... - - my_resource = MyResource.as_view({'get': 'list', 'post': 'create'}) - """ - - @classonlymethod - def as_view(cls, actions=None, **initkwargs): - """ - Main entry point for a request-response process. - """ - # sanitize keyword arguments - for key in initkwargs: - if key in cls.http_method_names: - raise TypeError("You tried to pass in the %s method name as a " - "keyword argument to %s(). Don't do that." - % (key, cls.__name__)) - if not hasattr(cls, key): - raise TypeError("%s() received an invalid keyword %r" % ( - cls.__name__, key)) - - def view(request, *args, **kwargs): - self = cls(**initkwargs) - - # Bind methods to actions - for method, action in actions.items(): - handler = getattr(self, action) - setattr(self, method, handler) - - # As you were, solider. - if hasattr(self, 'get') and not hasattr(self, 'head'): - self.head = self.get - return self.dispatch(request, *args, **kwargs) - - # take name and docstring from class - update_wrapper(view, cls, updated=()) - - # and possible attributes set by decorators - # like csrf_exempt from dispatch - update_wrapper(view, cls.dispatch, assigned=()) - return view - - -class Resource(ResourceMixin, views.APIView): - pass - - -# Note the inheritence of both MultipleObjectAPIView *and* SingleObjectAPIView -# is a bit weird given the diamond inheritence, but it will work for now. -# There's some implementation clean up that can happen later. -class ModelResource(mixins.CreateModelMixin, - mixins.RetrieveModelMixin, - mixins.UpdateModelMixin, - mixins.DestroyModelMixin, - mixins.ListModelMixin, - ResourceMixin, - generics.MultipleObjectAPIView, - generics.SingleObjectAPIView): - pass diff --git a/rest_framework/routers.py b/rest_framework/routers.py new file mode 100644 index 000000000..63eae5d74 --- /dev/null +++ b/rest_framework/routers.py @@ -0,0 +1,43 @@ +from django.conf.urls import url, patterns + + +class BaseRouter(object): + def __init__(self): + self.registry = [] + + def register(self, prefix, viewset, base_name): + self.registry.append((prefix, viewset, base_name)) + + def get_urlpatterns(self): + raise NotImplemented('get_urlpatterns must be overridden') + + @property + def urlpatterns(self): + if not hasattr(self, '_urlpatterns'): + print self.get_urlpatterns() + self._urlpatterns = patterns('', *self.get_urlpatterns()) + return self._urlpatterns + + +class DefaultRouter(BaseRouter): + route_list = [ + (r'$', {'get': 'list', 'post': 'create'}, '%s-list'), + (r'(?P[^/]+)/$', {'get': 'retrieve', 'put': 'update', 'delete': 'destroy'}, '%s-detail'), + ] + + def get_urlpatterns(self): + ret = [] + for prefix, viewset, base_name in self.registry: + for suffix, action_mapping, name_format in self.route_list: + + # Only actions which actually exist on the viewset will be bound + bound_actions = {} + for method, action in action_mapping.items(): + if hasattr(viewset, action): + bound_actions[method] = action + + regex = prefix + suffix + view = viewset.as_view(bound_actions) + name = name_format % base_name + ret.append(url(regex, view, name=name)) + return ret diff --git a/rest_framework/viewsets.py b/rest_framework/viewsets.py index a5aef5b71..887a97223 100644 --- a/rest_framework/viewsets.py +++ b/rest_framework/viewsets.py @@ -1,33 +1,86 @@ -# Not properly implemented yet, just the basic idea +from functools import update_wrapper +from django.utils.decorators import classonlymethod +from rest_framework import views, generics, mixins -class BaseRouter(object): - def __init__(self): - self.resources = [] +class ViewSetMixin(object): + """ + This is the magic. - def register(self, name, resource): - self.resources.append((name, resource)) + Overrides `.as_view()` so that it takes an `actions` keyword that performs + the binding of HTTP methods to actions on the Resource. - @property - def urlpatterns(self): - ret = [] + For example, to create a concrete view binding the 'GET' and 'POST' methods + to the 'list' and 'create' actions... - for name, resource in self.resources: - list_actions = { - 'get': getattr(resource, 'list', None), - 'post': getattr(resource, 'create', None) - } - detail_actions = { - 'get': getattr(resource, 'retrieve', None), - 'put': getattr(resource, 'update', None), - 'delete': getattr(resource, 'destroy', None) - } - list_regex = r'^%s/$' % name - detail_regex = r'^%s/(?P[0-9]+)/$' % name - list_name = '%s-list' - detail_name = '%s-detail' + view = MyViewSet.as_view({'get': 'list', 'post': 'create'}) + """ - ret += url(list_regex, resource.as_view(list_actions), list_name) - ret += url(detail_regex, resource.as_view(detail_actions), detail_name) + @classonlymethod + def as_view(cls, actions=None, **initkwargs): + """ + Main entry point for a request-response process. - return ret + Because of the way class based views create a closure around the + instantiated view, we need to totally reimplement `.as_view`, + and slightly modify the view function that is created and returned. + """ + # sanitize keyword arguments + for key in initkwargs: + if key in cls.http_method_names: + raise TypeError("You tried to pass in the %s method name as a " + "keyword argument to %s(). Don't do that." + % (key, cls.__name__)) + if not hasattr(cls, key): + raise TypeError("%s() received an invalid keyword %r" % ( + cls.__name__, key)) + + def view(request, *args, **kwargs): + self = cls(**initkwargs) + + # Bind methods to actions + # This is the bit that's different to a standard view + for method, action in actions.items(): + handler = getattr(self, action) + setattr(self, method, handler) + + # Patch this in as it's otherwise only present from 1.5 onwards + if hasattr(self, 'get') and not hasattr(self, 'head'): + self.head = self.get + + # And continue as usual + return self.dispatch(request, *args, **kwargs) + + # take name and docstring from class + update_wrapper(view, cls, updated=()) + + # and possible attributes set by decorators + # like csrf_exempt from dispatch + update_wrapper(view, cls.dispatch, assigned=()) + return view + + +class ViewSet(ViewSetMixin, views.APIView): + pass + + +# Note the inheritence of both MultipleObjectAPIView *and* SingleObjectAPIView +# is a bit weird given the diamond inheritence, but it will work for now. +# There's some implementation clean up that can happen later. +class ModelViewSet(mixins.CreateModelMixin, + mixins.RetrieveModelMixin, + mixins.UpdateModelMixin, + mixins.DestroyModelMixin, + mixins.ListModelMixin, + ViewSetMixin, + generics.MultipleObjectAPIView, + generics.SingleObjectAPIView): + pass + + +class ReadOnlyModelViewSet(mixins.RetrieveModelMixin, + mixins.ListModelMixin, + ViewSetMixin, + generics.MultipleObjectAPIView, + generics.SingleObjectAPIView): + pass