diff options
| author | Tom Christie | 2013-04-04 20:00:44 +0100 | 
|---|---|---|
| committer | Tom Christie | 2013-04-04 20:00:44 +0100 | 
| commit | c785628300d2b7cce63862a18915c537f8a3ab24 (patch) | |
| tree | 266aaf0f12ecb7228d50bffef671e8f611286f68 | |
| parent | ec076a00786c6b89a55b6ffe2556bb3b777100f5 (diff) | |
| download | django-rest-framework-c785628300d2b7cce63862a18915c537f8a3ab24.tar.bz2 | |
Fleshing out viewsets/routers
| -rw-r--r-- | docs/api-guide/viewsets-routers.md | 50 | ||||
| -rw-r--r-- | rest_framework/resources.py | 75 | ||||
| -rw-r--r-- | rest_framework/routers.py | 43 | ||||
| -rw-r--r-- | rest_framework/viewsets.py | 119 | 
4 files changed, 178 insertions, 109 deletions
diff --git a/docs/api-guide/viewsets-routers.md b/docs/api-guide/viewsets-routers.md index 817e1b8f..7813c00d 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 d4019a94..00000000 --- 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 00000000..63eae5d7 --- /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<pk>[^/]+)/$', {'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 a5aef5b7..887a9722 100644 --- a/rest_framework/viewsets.py +++ b/rest_framework/viewsets.py @@ -1,33 +1,86 @@ -# Not properly implemented yet, just the basic idea - - -class BaseRouter(object): -    def __init__(self): -        self.resources = [] - -    def register(self, name, resource): -        self.resources.append((name, resource)) - -    @property -    def urlpatterns(self): -        ret = [] - -        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<pk>[0-9]+)/$' % name -            list_name = '%s-list' -            detail_name = '%s-detail' - -            ret += url(list_regex, resource.as_view(list_actions), list_name) -            ret += url(detail_regex, resource.as_view(detail_actions), detail_name) - -        return ret +from functools import update_wrapper +from django.utils.decorators import classonlymethod +from rest_framework import views, generics, mixins + + +class ViewSetMixin(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... + +    view = MyViewSet.as_view({'get': 'list', 'post': 'create'}) +    """ + +    @classonlymethod +    def as_view(cls, actions=None, **initkwargs): +        """ +        Main entry point for a request-response process. + +        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  | 
