From 8f3931e02d0f0ba803075ca65dc8617ee959456f Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 15 Jan 2013 17:50:39 +0000 Subject: [PATCH 01/23] Update docs --- .../6-resource-orientated-projects.md | 119 +++++++++++------- 1 file changed, 76 insertions(+), 43 deletions(-) diff --git a/docs/tutorial/6-resource-orientated-projects.md b/docs/tutorial/6-resource-orientated-projects.md index 9ee599ae3..97fb5d69c 100644 --- a/docs/tutorial/6-resource-orientated-projects.md +++ b/docs/tutorial/6-resource-orientated-projects.md @@ -1,49 +1,93 @@ # Tutorial 6 - Resources -Resource classes are just View classes that don't have any handler methods bound to them. The actions on a resource are defined, +REST framework includes an abstraction for dealing with resources, that allows the developer to concentrate on modelling the state and interactions of the API, and leave the URL construction to be handled automatically, based on common conventions. -This allows us to: +To work with resources, we can use either the `Resource` class, which does not define any default handlers, or the `ModelResource` class, which provides a default set of CRUD operations. -* Encapsulate common behaviour across a class of views, in a single Resource class. -* Separate out the actions of a Resource from the specifics of how those actions should be bound to a particular set of URLs. +Resource classes are very similar to class based views, except that they provide operations such as `read`, or `update`, and not HTTP method handlers such as `get` or `put`. Resources are only bound to HTTP method handlers at the last moment, when they are instantiated into views, typically by using a `Router` class which handles the complexities of defining the URL conf for you. -## Refactoring to use Resources, not Views +## Refactoring to use Resources, instead of Views -For instance, we can re-write our 4 sets of views into something more compact... +Let's take our current set of views, and refactor them into resources. +We'll remove our existing `views.py` module, and instead create a `resources.py` -resources.py +Our `UserResource` is simple, since we just want the default model CRUD behavior, so we inherit from `ModelResource` and include the same set of attributes we used for the corresponding view classes. - class BlogPostResource(ModelResource): - serializer_class = BlogPostSerializer - model = BlogPost - permissions_classes = (permissions.IsAuthenticatedOrReadOnly,) - throttle_classes = (throttles.UserRateThrottle,) + class UserResource(resources.ModelResource): + model = User + serializer_class = UserSerializer + +There's a little bit more work to do for the `SnippetResource`. Again, we want the +default set of CRUD behavior, but we also want to include an endpoint for snippet highlights. + + class SnippetResource(resources.ModelResource): + model = Snippet + serializer_class = SnippetSerializer + permission_classes = (permissions.IsAuthenticatedOrReadOnly, + IsOwnerOrReadOnly,) + + @link(renderer_classes=[renderers.StaticHTMLRenderer]) + def highlight(self, request, *args, **kwargs): + snippet = self.get_object() + return Response(snippet.highlighted) + + def pre_save(self, obj): + obj.owner = self.request.user + +Notice that we've used the `@link` decorator for the `highlight` endpoint. This decorator can be used for non-CRUD endpoints that are "safe" operations that do not change server state. Using `@link` indicates that we want to use a `GET` method for these operations. For non-CRUD operations we can also use the `@action` decorator for any operations that change server state, which ensures that the `POST` method will be used for the operation. - class CommentResource(ModelResource): - serializer_class = CommentSerializer - model = Comment - permissions_classes = (permissions.IsAuthenticatedOrReadOnly,) - throttle_classes = (throttles.UserRateThrottle,) ## Binding Resources to URLs explicitly -The handler methods only get bound to the actions when we define the URLConf. Here's our urls.py: - comment_root = CommentResource.as_view(actions={ +The handler methods only get bound to the actions when we define the URLConf. +To see what's going on under the hood let's first explicitly create a set of views from our resources. + +In the `urls.py` file we first need to bind our resources to concrete views. + + snippet_list = SnippetResource.as_view(actions={ 'get': 'list', 'post': 'create' }) - comment_instance = CommentInstance.as_view(actions={ + snippet_detail = SnippetResource.as_view(actions={ 'get': 'retrieve', 'put': 'update', 'delete': 'destroy' }) - ... # And for blog post - - urlpatterns = patterns('blogpost.views', - url(r'^$', comment_root), - url(r'^(?P[0-9]+)$', comment_instance) - ... # And for blog post - ) + snippet_highlight = SnippetResource.as_view(actions={ + 'get': 'highlight' + }) + user_list = UserResource.as_view(actions={ + 'get': 'list', + 'post': 'create' + }) + user_detail = UserResource.as_view(actions={ + 'get': 'retrieve', + 'put': 'update', + 'delete': 'destroy' + }) + +We've now got a set of views exactly as we did before, that we can register with the URL conf. + +Replace the remainder of the `urls.py` file with the following: + + urlpatterns = format_suffix_patterns(patterns('snippets.views', + url(r'^$', 'api_root'), + url(r'^snippets/$', + snippet_list, + name='snippet-list'), + url(r'^snippets/(?P[0-9]+)/$', + snippet_detail, + name='snippet-detail'), + url(r'^snippets/(?P[0-9]+)/highlight/$', + snippet_highlight, + name='snippet-highlight'), + url(r'^users/$', + user_list, + name='user-list'), + url(r'^users/(?P[0-9]+)/$', + user_detail, + name='user-detail') + )) ## Using Routers @@ -52,25 +96,14 @@ Right now that hasn't really saved us a lot of code. However, now that we're us from blog import resources from rest_framework.routers import DefaultRouter - router = DefaultRouter() - router.register(resources.BlogPostResource) - router.register(resources.CommentResource) + router = DefaultRouter(include_root=True, include_format_suffixes=True) + router.register(resources.SnippetResource) + router.register(resources.UserResource) urlpatterns = router.urlpatterns ## Trade-offs between views vs resources. -Writing resource-oriented code can be a good thing. It helps ensure that URL conventions will be consistent across your APIs, and minimises the amount of code you need to write. +Writing resource-oriented code can be a good thing. It helps ensure that URL conventions will be consistent across your APIs, minimises the amount of code you need to write, and allows you to concentrate on the interactions and representations your API provides rather than the specifics of the URL conf. -The trade-off is that the behaviour is less explict. It can be more difficult to determine what code path is being followed, or where to override some behaviour. +That doesn't mean it's always the right approach to take. There's a similar set of trade-offs to consider as when using class-based views. Using resources is less explicit than building your views individually. -## Onwards and upwards. - -We've reached the end of our tutorial. If you want to get more involved in the REST framework project, here's a few places you can start: - -* Contribute on GitHub by reviewing issues, and submitting issues or pull requests. -* Join the REST framework group, and help build the community. -* Follow me [on Twitter][twitter] and say hi. - -**Now go build some awesome things.** - -[twitter]: https://twitter.com/_tomchristie From 52847a215d4e8de88e81d9ae79ce8bee9a36a9a2 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 15 Jan 2013 17:50:51 +0000 Subject: [PATCH 02/23] Fix implementation --- rest_framework/mixins.py | 3 -- rest_framework/resources.py | 67 +++++++++++++------------------------ 2 files changed, 23 insertions(+), 47 deletions(-) diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py index 8873e4aed..9bd566dab 100644 --- a/rest_framework/mixins.py +++ b/rest_framework/mixins.py @@ -25,9 +25,6 @@ class CreateModelMixin(object): return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - def pre_save(self, obj): - pass - class ListModelMixin(object): """ diff --git a/rest_framework/resources.py b/rest_framework/resources.py index dd8a54716..d4019a94e 100644 --- a/rest_framework/resources.py +++ b/rest_framework/resources.py @@ -1,31 +1,27 @@ ##### RESOURCES AND ROUTERS ARE NOT YET IMPLEMENTED - PLACEHOLDER ONLY ##### from functools import update_wrapper -import inspect from django.utils.decorators import classonlymethod -from rest_framework import views, generics - - -def wrapped(source, dest): - """ - Copy public, non-method attributes from source to dest, and return dest. - """ - for attr in [attr for attr in dir(source) - if not attr.startswith('_') and not inspect.ismethod(attr)]: - setattr(dest, attr, getattr(source, attr)) - return dest +from rest_framework import views, generics, mixins ##### RESOURCES AND ROUTERS ARE NOT YET IMPLEMENTED - PLACEHOLDER ONLY ##### class ResourceMixin(object): """ - Clone Django's `View.as_view()` behaviour *except* using REST framework's - 'method -> action' binding for resources. + 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, **initkwargs): + def as_view(cls, actions=None, **initkwargs): """ Main entry point for a request-response process. """ @@ -61,36 +57,19 @@ class ResourceMixin(object): return view -##### RESOURCES AND ROUTERS ARE NOT YET IMPLEMENTED - PLACEHOLDER ONLY ##### - class Resource(ResourceMixin, views.APIView): pass -##### RESOURCES AND ROUTERS ARE NOT YET IMPLEMENTED - PLACEHOLDER ONLY ##### - -class ModelResource(ResourceMixin, views.APIView): - # TODO: Actually delegation won't work - root_class = generics.ListCreateAPIView - detail_class = generics.RetrieveUpdateDestroyAPIView - - def root_view(self): - return wrapped(self, self.root_class()) - - def detail_view(self): - return wrapped(self, self.detail_class()) - - def list(self, request, *args, **kwargs): - return self.root_view().list(request, args, kwargs) - - def create(self, request, *args, **kwargs): - return self.root_view().create(request, args, kwargs) - - def retrieve(self, request, *args, **kwargs): - return self.detail_view().retrieve(request, args, kwargs) - - def update(self, request, *args, **kwargs): - return self.detail_view().update(request, args, kwargs) - - def destroy(self, request, *args, **kwargs): - return self.detail_view().destroy(request, args, kwargs) +# 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 From 4a7139e41d2500776c30e663c1cebce74b49270d Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 15 Jan 2013 21:49:24 +0000 Subject: [PATCH 03/23] Tweaks --- .../6-resource-orientated-projects.md | 20 ++++++----- rest_framework/routers.py | 33 +++++++++++++++++++ 2 files changed, 44 insertions(+), 9 deletions(-) create mode 100644 rest_framework/routers.py diff --git a/docs/tutorial/6-resource-orientated-projects.md b/docs/tutorial/6-resource-orientated-projects.md index 97fb5d69c..019371d76 100644 --- a/docs/tutorial/6-resource-orientated-projects.md +++ b/docs/tutorial/6-resource-orientated-projects.md @@ -44,23 +44,25 @@ To see what's going on under the hood let's first explicitly create a set of vie In the `urls.py` file we first need to bind our resources to concrete views. - snippet_list = SnippetResource.as_view(actions={ + from snippets import resources + + snippet_list = resources.SnippetResource.as_view({ 'get': 'list', 'post': 'create' }) - snippet_detail = SnippetResource.as_view(actions={ + snippet_detail = resources.SnippetResource.as_view({ 'get': 'retrieve', 'put': 'update', 'delete': 'destroy' }) - snippet_highlight = SnippetResource.as_view(actions={ + snippet_highlight = resources.SnippetResource.as_view({ 'get': 'highlight' }) - user_list = UserResource.as_view(actions={ + user_list = resources.UserResource.as_view({ 'get': 'list', 'post': 'create' }) - user_detail = UserResource.as_view(actions={ + user_detail = resources.UserResource.as_view({ 'get': 'retrieve', 'put': 'update', 'delete': 'destroy' @@ -93,12 +95,12 @@ Replace the remainder of the `urls.py` file with the following: Right now that hasn't really saved us a lot of code. However, now that we're using Resources rather than Views, we actually don't need to design the urlconf ourselves. The conventions for wiring up resources into views and urls can be handled automatically, using `Router` classes. All we need to do is register the appropriate resources with a router, and let it do the rest. Here's our re-wired `urls.py` file. - from blog import resources + from snippets import resources from rest_framework.routers import DefaultRouter - router = DefaultRouter(include_root=True, include_format_suffixes=True) - router.register(resources.SnippetResource) - router.register(resources.UserResource) + router = DefaultRouter() + router.register('snippets', resources.SnippetResource) + router.register('users', resources.UserResource) urlpatterns = router.urlpatterns ## Trade-offs between views vs resources. diff --git a/rest_framework/routers.py b/rest_framework/routers.py new file mode 100644 index 000000000..a5aef5b71 --- /dev/null +++ b/rest_framework/routers.py @@ -0,0 +1,33 @@ +# 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[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 4c6396108704d38f534a16577de59178b1d0df3b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 17 Jan 2013 12:30:28 +0000 Subject: [PATCH 04/23] Tweak resource docs --- .../6-resource-orientated-projects.md | 62 ++++++------------- 1 file changed, 19 insertions(+), 43 deletions(-) diff --git a/docs/tutorial/6-resource-orientated-projects.md b/docs/tutorial/6-resource-orientated-projects.md index 019371d76..37ab1419e 100644 --- a/docs/tutorial/6-resource-orientated-projects.md +++ b/docs/tutorial/6-resource-orientated-projects.md @@ -42,65 +42,41 @@ Notice that we've used the `@link` decorator for the `highlight` endpoint. This The handler methods only get bound to the actions when we define the URLConf. To see what's going on under the hood let's first explicitly create a set of views from our resources. -In the `urls.py` file we first need to bind our resources to concrete views. +In the `urls.py` file we first need to bind our resource classes into a set of concrete views. - from snippets import resources + from snippets.resources import SnippetResource, UserResource - snippet_list = resources.SnippetResource.as_view({ - 'get': 'list', - 'post': 'create' - }) - snippet_detail = resources.SnippetResource.as_view({ - 'get': 'retrieve', - 'put': 'update', - 'delete': 'destroy' - }) - snippet_highlight = resources.SnippetResource.as_view({ - 'get': 'highlight' - }) - user_list = resources.UserResource.as_view({ - 'get': 'list', - 'post': 'create' - }) - user_detail = resources.UserResource.as_view({ - 'get': 'retrieve', - 'put': 'update', - 'delete': 'destroy' - }) + snippet_list = SnippetResource.as_view({'get': 'list', 'post': 'create'}) + snippet_detail = SnippetResource.as_view({'get': 'retrieve', 'put': 'update', 'delete': 'destroy'}) + snippet_highlight = SnippetResource.as_view({'get': 'highlight'}) + user_list = UserResource.as_view({'get': 'list', 'post': 'create'}) + user_detail = UserResource.as_view({'get': 'retrieve', 'put': 'update', 'delete': 'destroy'}) -We've now got a set of views exactly as we did before, that we can register with the URL conf. +Notice how create multiple views onto a single resource class, by binding the http methods to the required action for each view. -Replace the remainder of the `urls.py` file with the following: +Now that we've bound our resources into concrete views, that we can register the views with the URL conf as usual. urlpatterns = format_suffix_patterns(patterns('snippets.views', url(r'^$', 'api_root'), - url(r'^snippets/$', - snippet_list, - name='snippet-list'), - url(r'^snippets/(?P[0-9]+)/$', - snippet_detail, - name='snippet-detail'), - url(r'^snippets/(?P[0-9]+)/highlight/$', - snippet_highlight, - name='snippet-highlight'), - url(r'^users/$', - user_list, - name='user-list'), - url(r'^users/(?P[0-9]+)/$', - user_detail, - name='user-detail') + url(r'^snippets/$', snippet_list, name='snippet-list'), + url(r'^snippets/(?P[0-9]+)/$', snippet_detail, name='snippet-detail'), + url(r'^snippets/(?P[0-9]+)/highlight/$', snippet_highlight, name='snippet-highlight'), + url(r'^users/$', user_list, name='user-list'), + url(r'^users/(?P[0-9]+)/$', user_detail, name='user-detail') )) ## Using Routers -Right now that hasn't really saved us a lot of code. However, now that we're using Resources rather than Views, we actually don't need to design the urlconf ourselves. The conventions for wiring up resources into views and urls can be handled automatically, using `Router` classes. All we need to do is register the appropriate resources with a router, and let it do the rest. Here's our re-wired `urls.py` file. +Now that we're using Resources rather than Views, we actually don't need to design the URL conf ourselves. The conventions for wiring up resources into views and urls can be handled automatically, using `Router` classes. All we need to do is register the appropriate resources with a router, and let it do the rest. + +Here's our re-wired `urls.py` file. from snippets import resources from rest_framework.routers import DefaultRouter router = DefaultRouter() - router.register('snippets', resources.SnippetResource) - router.register('users', resources.UserResource) + router.register(r'^snippets/', resources.SnippetResource, 'snippet') + router.register(r'^users/', resources.UserResource, 'user') urlpatterns = router.urlpatterns ## Trade-offs between views vs resources. From 922ee61d8611b41e2944b6503af736b1790abe83 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 18 Mar 2013 21:05:13 +0000 Subject: [PATCH 05/23] Remove erronous pre_save --- rest_framework/generics.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/rest_framework/generics.py b/rest_framework/generics.py index 55918267a..36ecf9150 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -82,9 +82,6 @@ class GenericAPIView(views.APIView): """ pass - def pre_save(self, obj): - pass - class MultipleObjectAPIView(MultipleObjectMixin, GenericAPIView): """ From e9092e14207958832a86258fcb3740fb76dbcbe0 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 30 Mar 2013 16:54:22 +0000 Subject: [PATCH 06/23] Initial API documentation --- docs/viewsets-routers.md | 107 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 docs/viewsets-routers.md diff --git a/docs/viewsets-routers.md b/docs/viewsets-routers.md new file mode 100644 index 000000000..84ccb10b2 --- /dev/null +++ b/docs/viewsets-routers.md @@ -0,0 +1,107 @@ +# ViewSets & Routers + +> Resource routing allows you to quickly declare all of the common routes for a given resourceful controller. Instead of declaring separate routes for your index... a resourceful route declares them in a single line of code. + +— [Ruby on Rails Documentation][cite] + +Some Web frameworks such as Rails provide functionality for automatically determining how the URLs for an application should be mapped to the logic that deals with handling incoming requests. + +Conversely, Django stops short of automatically generating URLs, and requires you to explicitly manage your URL configuration. + +REST framework adds support for automatic URL routing, which provides you with a simple, quick and consistent way of wiring your view logic to a set of URLs. + +# ViewSets + +Django REST framework allows you to combine the logic for a set of related views in a single class, called a `ViewSet`. In other frameworks you may also find conceptually similar implementations named something like 'Resources' or 'Controllers'. + +A `ViewSet` class is simply **a type of class-based View, that does not provide any method handlers** such as `.get()` or `.post()`, and instead provides actions such as `.list()` and `.create()`. + +The method handlers for a `ViewSet` are only bound to the corresponding actions at the point of finalizing the view, using the `.as_view()` method. + +Typically, rather than exlicitly registering the views in a viewset in the urlconf, you'll register the viewset with a router class, that automatically determines the urlconf for you. + +## Example + +Let's define a simple viewset that can be used to listing or retrieving all the users in the system. + + class UserViewSet(ViewSet): + """ + A simple ViewSet that for listing or retrieving users. + """ + queryset = User.objects.all() + + def list(self, request): + serializer = UserSerializer(self.queryset, many=True) + return Response(serializer.data) + + def retrieve(self, request, pk=None): + user = get_object_or_404(self.queryset, pk=pk) + serializer = UserSerializer(user) + return Response(serializer.data) + +If we need to, we can bind this viewset into two seperate views, like so: + + user_list = UserViewSet.as_view({'get': 'list'}) + user_detail = UserViewSet.as_view({'get': 'retrieve'}) + +Typically we wouldn't do this, but would instead register the viewset with a router, and allow the urlconf to be automatically generated. + +# API Reference + +## ViewSet + +The `ViewSet` class inherits from `APIView`. You can use any of the standard attributes such as `permission_classes`, `authentication_classes` in order to control the API policy on the viewset. + +The `ViewSet` class does not provide any implementations of actions. In order to use a `ViewSet` class you'll override the class and define the action implementations explicitly. + +## ModelViewSet + +The `ModelViewSet` class inherits from `GenericAPIView` and includes implementations for various actions, by mixing in the behavior of the + +The actions provided by the `ModelViewSet` class are `.list()`, `.retrieve()`, `.create()`, `.update()`, and `.destroy()`. + +## 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()`. + +# 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. + +For example, the definition of `ModelViewSet` looks like this: + + class ModelViewSet(mixins.CreateModelMixin, + mixins.RetrieveModelMixin, + mixins.UpdateModelMixin, + mixins.DestroyModelMixin, + mixins.ListModelMixin, + viewsets.ViewSetMixin, + generics.GenericAPIView): + """ + A viewset that provides actions for `create`, `retrieve`, + `update`, `destroy` and `list` actions. + + To use it, override the class and set the `.queryset` + and `.serializer_class` attributes. + """ + pass + +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. + +--- + +# Routers + +Routers provide a convenient and simple shortcut for wiring up your application's URLs. + + router = routers.DefaultRouter() + router.register('^/', APIRoot, 'api-root') + router.register('^users/', UserViewSet, 'user') + router.register('^groups/', GroupViewSet, 'group') + router.register('^accounts/', AccountViewSet, 'account') + + urlpatterns = router.urlpatterns + +[cite]: http://guides.rubyonrails.org/routing.html \ No newline at end of file From 97aa0239163868af40b0a5660c48b54bd7656ad6 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 30 Mar 2013 17:22:52 +0000 Subject: [PATCH 07/23] Updating tutorial --- .../6-resource-orientated-projects.md | 69 ++++++++++--------- 1 file changed, 38 insertions(+), 31 deletions(-) diff --git a/docs/tutorial/6-resource-orientated-projects.md b/docs/tutorial/6-resource-orientated-projects.md index 37ab1419e..9c8a218f9 100644 --- a/docs/tutorial/6-resource-orientated-projects.md +++ b/docs/tutorial/6-resource-orientated-projects.md @@ -1,27 +1,25 @@ -# Tutorial 6 - Resources +# Tutorial 6 - ViewSets & Routers -REST framework includes an abstraction for dealing with resources, that allows the developer to concentrate on modelling the state and interactions of the API, and leave the URL construction to be handled automatically, based on common conventions. +REST framework includes an abstraction for dealing with `ViewSets`, that allows the developer to concentrate on modelling the state and interactions of the API, and leave the URL construction to be handled automatically, based on common conventions. -To work with resources, we can use either the `Resource` class, which does not define any default handlers, or the `ModelResource` class, which provides a default set of CRUD operations. +`ViewSet` classes are almost the same thing as `View` classes, except that they provide operations such as `read`, or `update`, and not method handlers such as `get` or `put`. -Resource classes are very similar to class based views, except that they provide operations such as `read`, or `update`, and not HTTP method handlers such as `get` or `put`. Resources are only bound to HTTP method handlers at the last moment, when they are instantiated into views, typically by using a `Router` class which handles the complexities of defining the URL conf for you. +A `ViewSet` class is only bound to a set of method handlers at the last moment, when it is instantiated into a set of views, typically by using a `Router` class which handles the complexities of defining the URL conf for you. -## Refactoring to use Resources, instead of Views +## Refactoring to use ViewSets -Let's take our current set of views, and refactor them into resources. -We'll remove our existing `views.py` module, and instead create a `resources.py` +Let's take our current set of views, and refactor them into view sets. -Our `UserResource` is simple, since we just want the default model CRUD behavior, so we inherit from `ModelResource` and include the same set of attributes we used for the corresponding view classes. +First of all let's refactor our `UserListView` and `UserDetailView` views into a single `UserViewSet`. We can remove the two views, and replace then with a single class: - class UserResource(resources.ModelResource): - model = User + class UserViewSet(viewsets.ModelViewSet): + queryset = User.objects.all() serializer_class = UserSerializer -There's a little bit more work to do for the `SnippetResource`. Again, we want the -default set of CRUD behavior, but we also want to include an endpoint for snippet highlights. +Next we're going to replace the `SnippetList`, `SnippetDetail` and `SnippetHighlight` view classes. We can remove the three views, and again replace them with a single class. - class SnippetResource(resources.ModelResource): - model = Snippet + class SnippetViewSet(viewsets.ModelViewSet): + queryset = Snippet.objects.all() serializer_class = SnippetSerializer permission_classes = (permissions.IsAuthenticatedOrReadOnly, IsOwnerOrReadOnly,) @@ -34,25 +32,27 @@ default set of CRUD behavior, but we also want to include an endpoint for snippe def pre_save(self, obj): obj.owner = self.request.user -Notice that we've used the `@link` decorator for the `highlight` endpoint. This decorator can be used for non-CRUD endpoints that are "safe" operations that do not change server state. Using `@link` indicates that we want to use a `GET` method for these operations. For non-CRUD operations we can also use the `@action` decorator for any operations that change server state, which ensures that the `POST` method will be used for the operation. +Notice that we've used the `@link` decorator for the `highlight` method. +This decorator can be used to add custom endpoints, other than the standard `create`/`update`/`delete` endpoints. +The `@link` decorator will -## Binding Resources to URLs explicitly +## Binding ViewSets to URLs explicitly The handler methods only get bound to the actions when we define the URLConf. -To see what's going on under the hood let's first explicitly create a set of views from our resources. +To see what's going on under the hood let's first explicitly create a set of views from our ViewSets. -In the `urls.py` file we first need to bind our resource classes into a set of concrete views. +In the `urls.py` file we first need to bind our `ViewSet` classes into a set of concrete views. from snippets.resources import SnippetResource, UserResource - snippet_list = SnippetResource.as_view({'get': 'list', 'post': 'create'}) - snippet_detail = SnippetResource.as_view({'get': 'retrieve', 'put': 'update', 'delete': 'destroy'}) - snippet_highlight = SnippetResource.as_view({'get': 'highlight'}) - user_list = UserResource.as_view({'get': 'list', 'post': 'create'}) - user_detail = UserResource.as_view({'get': 'retrieve', 'put': 'update', 'delete': 'destroy'}) + snippet_list = SnippetViewSet.as_view({'get': 'list', 'post': 'create'}) + snippet_detail = SnippetViewSet.as_view({'get': 'retrieve', 'put': 'update', 'delete': 'destroy'}) + snippet_highlight = SnippetViewSet.as_view({'get': 'highlight'}) + user_list = UserViewSet.as_view({'get': 'list', 'post': 'create'}) + user_detail = UserViewSet.as_view({'get': 'retrieve', 'put': 'update', 'delete': 'destroy'}) -Notice how create multiple views onto a single resource class, by binding the http methods to the required action for each view. +Notice how we're creating multiple views from each `ViewSet` class, by binding the http methods to the required action for each view. Now that we've bound our resources into concrete views, that we can register the views with the URL conf as usual. @@ -67,21 +67,28 @@ Now that we've bound our resources into concrete views, that we can register the ## Using Routers -Now that we're using Resources rather than Views, we actually don't need to design the URL conf ourselves. The conventions for wiring up resources into views and urls can be handled automatically, using `Router` classes. All we need to do is register the appropriate resources with a router, and let it do the rest. +Now that we're using `ViewSet` classes rather than `View` classes, we actually don't need to design the URL conf ourselves. The conventions for wiring up resources into views and urls can be handled automatically, using a `Router` class. All we need to do is register the appropriate view sets with a router, and let it do the rest. Here's our re-wired `urls.py` file. - from snippets import resources + from snippets import views from rest_framework.routers import DefaultRouter + # Create a router and register our views and view sets with it. router = DefaultRouter() - router.register(r'^snippets/', resources.SnippetResource, 'snippet') - router.register(r'^users/', resources.UserResource, 'user') + router.register(r'^/', views.api_root) + router.register(r'^snippets/', views.SnippetViewSet, 'snippet') + router.register(r'^users/', views.UserViewSet, 'user') + + # The urlconf is determined automatically by the router. urlpatterns = router.urlpatterns + + # Add format suffixes to all our URL patterns. + urlpatterns = format_suffix_patterns(urlpatterns) -## Trade-offs between views vs resources. +## Trade-offs between views vs viewsets. -Writing resource-oriented code can be a good thing. It helps ensure that URL conventions will be consistent across your APIs, minimises the amount of code you need to write, and allows you to concentrate on the interactions and representations your API provides rather than the specifics of the URL conf. +Using view sets can be a really useful abstraction. It helps ensure that URL conventions will be consistent across your API, minimises the amount of code you need to write, and allows you to concentrate on the interactions and representations your API provides rather than the specifics of the URL conf. -That doesn't mean it's always the right approach to take. There's a similar set of trade-offs to consider as when using class-based views. Using resources is less explicit than building your views individually. +That doesn't mean it's always the right approach to take. There's a similar set of trade-offs to consider as when using class-based views instead of function based views. Using view sets is less explicit than building your views individually. From ec076a00786c6b89a55b6ffe2556bb3b777100f5 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sun, 31 Mar 2013 11:36:58 +0100 Subject: [PATCH 08/23] Add viewsets/routers to indexs etc --- docs/{ => api-guide}/viewsets-routers.md | 6 ++++-- docs/index.md | 4 ++++ docs/template.html | 2 ++ ...rce-orientated-projects.md => 6-viewsets-and-routers.md} | 0 mkdocs.py | 2 ++ rest_framework/{routers.py => viewsets.py} | 0 6 files changed, 12 insertions(+), 2 deletions(-) rename docs/{ => api-guide}/viewsets-routers.md (97%) rename docs/tutorial/{6-resource-orientated-projects.md => 6-viewsets-and-routers.md} (100%) rename rest_framework/{routers.py => viewsets.py} (100%) diff --git a/docs/viewsets-routers.md b/docs/api-guide/viewsets-routers.md similarity index 97% rename from docs/viewsets-routers.md rename to docs/api-guide/viewsets-routers.md index 84ccb10b2..817e1b8f7 100644 --- a/docs/viewsets-routers.md +++ b/docs/api-guide/viewsets-routers.md @@ -1,8 +1,10 @@ + + # ViewSets & Routers > Resource routing allows you to quickly declare all of the common routes for a given resourceful controller. Instead of declaring separate routes for your index... a resourceful route declares them in a single line of code. - -— [Ruby on Rails Documentation][cite] +> +> — [Ruby on Rails Documentation][cite] Some Web frameworks such as Rails provide functionality for automatically determining how the URLs for an application should be mapped to the logic that deals with handling incoming requests. diff --git a/docs/index.md b/docs/index.md index 4c2720c89..469a58852 100644 --- a/docs/index.md +++ b/docs/index.md @@ -86,6 +86,7 @@ The tutorial will walk you through the building blocks that make up REST framewo * [3 - Class based views][tut-3] * [4 - Authentication & permissions][tut-4] * [5 - Relationships & hyperlinked APIs][tut-5] +* [6 - ViewSets & Routers][tut-6] ## API Guide @@ -95,6 +96,7 @@ The API guide is your complete reference manual to all the functionality provide * [Responses][response] * [Views][views] * [Generic views][generic-views] +* [ViewSets and Routers][viewsets-routers] * [Parsers][parsers] * [Renderers][renderers] * [Serializers][serializers] @@ -197,11 +199,13 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. [tut-3]: tutorial/3-class-based-views.md [tut-4]: tutorial/4-authentication-and-permissions.md [tut-5]: tutorial/5-relationships-and-hyperlinked-apis.md +[tut-6]: tutorial/6-viewsets-and-routers.md [request]: api-guide/requests.md [response]: api-guide/responses.md [views]: api-guide/views.md [generic-views]: api-guide/generic-views.md +[viewsets-routers]: api-guide/viewsets-routers.md [parsers]: api-guide/parsers.md [renderers]: api-guide/renderers.md [serializers]: api-guide/serializers.md diff --git a/docs/template.html b/docs/template.html index 7e9297627..aec3ecc91 100644 --- a/docs/template.html +++ b/docs/template.html @@ -62,6 +62,7 @@
  • 3 - Class based views
  • 4 - Authentication and permissions
  • 5 - Relationships and hyperlinked APIs
  • +
  • 6 - ViewSets and Routers
  • Responses
  • Views
  • Generic views
  • +
  • ViewSets and Routers
  • Parsers
  • Renderers
  • Serializers
  • diff --git a/docs/tutorial/6-resource-orientated-projects.md b/docs/tutorial/6-viewsets-and-routers.md similarity index 100% rename from docs/tutorial/6-resource-orientated-projects.md rename to docs/tutorial/6-viewsets-and-routers.md diff --git a/mkdocs.py b/mkdocs.py index dadb17d27..f6cc2b5ae 100755 --- a/mkdocs.py +++ b/mkdocs.py @@ -47,10 +47,12 @@ path_list = [ 'tutorial/3-class-based-views.md', 'tutorial/4-authentication-and-permissions.md', 'tutorial/5-relationships-and-hyperlinked-apis.md', + 'tutorial/6-viewsets-and-routers.md', 'api-guide/requests.md', 'api-guide/responses.md', 'api-guide/views.md', 'api-guide/generic-views.md', + 'api-guide/viewsets-routers.md', 'api-guide/parsers.md', 'api-guide/renderers.md', 'api-guide/serializers.md', diff --git a/rest_framework/routers.py b/rest_framework/viewsets.py similarity index 100% rename from rest_framework/routers.py rename to rest_framework/viewsets.py From c785628300d2b7cce63862a18915c537f8a3ab24 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 4 Apr 2013 20:00:44 +0100 Subject: [PATCH 09/23] 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 From fb41d2ac8f495ae0728e3f38c6a21306f0507316 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 4 Apr 2013 20:35:40 +0100 Subject: [PATCH 10/23] Add support for action and link routing --- docs/tutorial/6-viewsets-and-routers.md | 15 ++++++++++++++- rest_framework/decorators.py | 22 ++++++++++++++++++++++ rest_framework/routers.py | 20 ++++++++++++++++++++ 3 files changed, 56 insertions(+), 1 deletion(-) diff --git a/docs/tutorial/6-viewsets-and-routers.md b/docs/tutorial/6-viewsets-and-routers.md index 9c8a218f9..8a2108b35 100644 --- a/docs/tutorial/6-viewsets-and-routers.md +++ b/docs/tutorial/6-viewsets-and-routers.md @@ -12,13 +12,26 @@ Let's take our current set of views, and refactor them into view sets. First of all let's refactor our `UserListView` and `UserDetailView` views into a single `UserViewSet`. We can remove the two views, and replace then with a single class: - class UserViewSet(viewsets.ModelViewSet): + class UserViewSet(viewsets.ReadOnlyModelViewSet): + """ + This viewset automatically provides `list` and `detail` actions. + """ queryset = User.objects.all() serializer_class = UserSerializer Next we're going to replace the `SnippetList`, `SnippetDetail` and `SnippetHighlight` view classes. We can remove the three views, and again replace them with a single class. + from rest_framework import viewsets + from rest_framework.decorators import link + class SnippetViewSet(viewsets.ModelViewSet): + """ + This viewset automatically provides `list`, `create`, `retrieve`, + `update` and `destroy` actions. + + Additionally we provide an extra `highlight` action, by using the + `@link` decorator. + """ queryset = Snippet.objects.all() serializer_class = SnippetSerializer permission_classes = (permissions.IsAuthenticatedOrReadOnly, diff --git a/rest_framework/decorators.py b/rest_framework/decorators.py index 8250cd3ba..00b37f8b4 100644 --- a/rest_framework/decorators.py +++ b/rest_framework/decorators.py @@ -97,3 +97,25 @@ def permission_classes(permission_classes): func.permission_classes = permission_classes return func return decorator + + +def link(**kwargs): + """ + Used to mark a method on a ViewSet that should be routed for GET requests. + """ + def decorator(func): + func.bind_to_method = 'get' + func.kwargs = kwargs + return func + return decorator + + +def action(**kwargs): + """ + Used to mark a method on a ViewSet that should be routed for POST requests. + """ + def decorator(func): + func.bind_to_method = 'post' + func.kwargs = kwargs + return func + return decorator diff --git a/rest_framework/routers.py b/rest_framework/routers.py index 63eae5d74..d1e961565 100644 --- a/rest_framework/routers.py +++ b/rest_framework/routers.py @@ -24,10 +24,12 @@ class DefaultRouter(BaseRouter): (r'$', {'get': 'list', 'post': 'create'}, '%s-list'), (r'(?P[^/]+)/$', {'get': 'retrieve', 'put': 'update', 'delete': 'destroy'}, '%s-detail'), ] + extra_routes = (r'(?P[^/]+)/%s/$', '%s-%s') def get_urlpatterns(self): ret = [] for prefix, viewset, base_name in self.registry: + # Bind standard routes for suffix, action_mapping, name_format in self.route_list: # Only actions which actually exist on the viewset will be bound @@ -36,8 +38,26 @@ class DefaultRouter(BaseRouter): if hasattr(viewset, action): bound_actions[method] = action + # Build the url pattern regex = prefix + suffix view = viewset.as_view(bound_actions) name = name_format % base_name ret.append(url(regex, view, name=name)) + + # Bind any extra @action or @link routes + for attr in dir(viewset): + func = getattr(viewset, attr) + http_method = getattr(func, 'bind_to_method', None) + if not http_method: + continue + + regex_format, name_format = self.extra_routes + + # Build the url pattern + regex = regex_format % attr + view = viewset.as_view({http_method: attr}, **func.kwargs) + name = name_format % (base_name, attr) + ret.append(url(regex, view, name=name)) + + # Return a list of url patterns return ret From 9e24db022cd8da1a588dd43e6239e07798881c02 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 4 Apr 2013 20:38:42 +0100 Subject: [PATCH 11/23] Commenting --- rest_framework/routers.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/rest_framework/routers.py b/rest_framework/routers.py index d1e961565..283add8de 100644 --- a/rest_framework/routers.py +++ b/rest_framework/routers.py @@ -29,7 +29,7 @@ class DefaultRouter(BaseRouter): def get_urlpatterns(self): ret = [] for prefix, viewset, base_name in self.registry: - # Bind standard routes + # Bind standard CRUD routes for suffix, action_mapping, name_format in self.route_list: # Only actions which actually exist on the viewset will be bound @@ -44,10 +44,12 @@ class DefaultRouter(BaseRouter): name = name_format % base_name ret.append(url(regex, view, name=name)) - # Bind any extra @action or @link routes + # Bind any extra `@action` or `@link` routes for attr in dir(viewset): func = getattr(viewset, attr) http_method = getattr(func, 'bind_to_method', None) + + # Skip if this is not an @action or @link method if not http_method: continue From f68721ade8d66806296323116ff9a61773ad2be1 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 4 Apr 2013 21:42:26 +0100 Subject: [PATCH 12/23] Factor view names/descriptions out of View class --- rest_framework/renderers.py | 11 +--- rest_framework/routers.py | 34 ++++++----- rest_framework/utils/breadcrumbs.py | 5 +- rest_framework/utils/formatting.py | 77 +++++++++++++++++++++++++ rest_framework/views.py | 89 +++-------------------------- rest_framework/viewsets.py | 5 +- 6 files changed, 117 insertions(+), 104 deletions(-) create mode 100644 rest_framework/utils/formatting.py diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 4c15e0db3..752306add 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -24,6 +24,7 @@ from rest_framework.settings import api_settings from rest_framework.request import clone_request from rest_framework.utils import encoders from rest_framework.utils.breadcrumbs import get_breadcrumbs +from rest_framework.utils.formatting import get_view_name, get_view_description from rest_framework import exceptions, parsers, status, VERSION @@ -438,16 +439,10 @@ class BrowsableAPIRenderer(BaseRenderer): return GenericContentForm() def get_name(self, view): - try: - return view.get_name() - except AttributeError: - return smart_text(view.__class__.__name__) + return get_view_name(view.__class__) def get_description(self, view): - try: - return view.get_description(html=True) - except AttributeError: - return smart_text(view.__doc__ or '') + return get_view_description(view.__class__, html=True) def render(self, data, accepted_media_type=None, renderer_context=None): """ diff --git a/rest_framework/routers.py b/rest_framework/routers.py index 283add8de..c37909ff7 100644 --- a/rest_framework/routers.py +++ b/rest_framework/routers.py @@ -14,23 +14,31 @@ class BaseRouter(object): @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'), + (r'$', {'get': 'list', 'post': 'create'}, 'list'), + (r'(?P[^/]+)/$', {'get': 'retrieve', 'put': 'update', 'delete': 'destroy'}, 'detail'), ] - extra_routes = (r'(?P[^/]+)/%s/$', '%s-%s') + extra_routes = r'(?P[^/]+)/%s/$' + name_format = '%s-%s' def get_urlpatterns(self): ret = [] for prefix, viewset, base_name in self.registry: + # Bind regular views + if not getattr(viewset, '_is_viewset', False): + regex = prefix + view = viewset + name = base_name + ret.append(url(regex, view, name=name)) + continue + # Bind standard CRUD routes - for suffix, action_mapping, name_format in self.route_list: + for suffix, action_mapping, action_name in self.route_list: # Only actions which actually exist on the viewset will be bound bound_actions = {} @@ -40,25 +48,25 @@ class DefaultRouter(BaseRouter): # Build the url pattern regex = prefix + suffix - view = viewset.as_view(bound_actions) - name = name_format % base_name + view = viewset.as_view(bound_actions, name_suffix=action_name) + name = self.name_format % (base_name, action_name) ret.append(url(regex, view, name=name)) # Bind any extra `@action` or `@link` routes - for attr in dir(viewset): - func = getattr(viewset, attr) + for action_name in dir(viewset): + func = getattr(viewset, action_name) http_method = getattr(func, 'bind_to_method', None) # Skip if this is not an @action or @link method if not http_method: continue - regex_format, name_format = self.extra_routes + suffix = self.extra_routes % action_name # Build the url pattern - regex = regex_format % attr - view = viewset.as_view({http_method: attr}, **func.kwargs) - name = name_format % (base_name, attr) + regex = prefix + suffix + view = viewset.as_view({http_method: action_name}, **func.kwargs) + name = self.name_format % (base_name, action_name) ret.append(url(regex, view, name=name)) # Return a list of url patterns diff --git a/rest_framework/utils/breadcrumbs.py b/rest_framework/utils/breadcrumbs.py index af21ac79b..18b3b2076 100644 --- a/rest_framework/utils/breadcrumbs.py +++ b/rest_framework/utils/breadcrumbs.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals from django.core.urlresolvers import resolve, get_script_prefix +from rest_framework.utils.formatting import get_view_name def get_breadcrumbs(url): @@ -16,11 +17,11 @@ def get_breadcrumbs(url): pass else: # Check if this is a REST framework view, and if so add it to the breadcrumbs - if isinstance(getattr(view, 'cls_instance', None), APIView): + if issubclass(getattr(view, 'cls', None), APIView): # Don't list the same view twice in a row. # Probably an optional trailing slash. if not seen or seen[-1] != view: - breadcrumbs_list.insert(0, (view.cls_instance.get_name(), prefix + url)) + breadcrumbs_list.insert(0, (get_view_name(view.cls), prefix + url)) seen.append(view) if url == '': diff --git a/rest_framework/utils/formatting.py b/rest_framework/utils/formatting.py new file mode 100644 index 000000000..79566db13 --- /dev/null +++ b/rest_framework/utils/formatting.py @@ -0,0 +1,77 @@ +""" +Utility functions to return a formatted name and description for a given view. +""" +from __future__ import unicode_literals + +from django.utils.html import escape +from django.utils.safestring import mark_safe +from rest_framework.compat import apply_markdown +import re + + +def _remove_trailing_string(content, trailing): + """ + Strip trailing component `trailing` from `content` if it exists. + Used when generating names from view classes. + """ + if content.endswith(trailing) and content != trailing: + return content[:-len(trailing)] + return content + + +def _remove_leading_indent(content): + """ + Remove leading indent from a block of text. + Used when generating descriptions from docstrings. + """ + whitespace_counts = [len(line) - len(line.lstrip(' ')) + for line in content.splitlines()[1:] if line.lstrip()] + + # unindent the content if needed + if whitespace_counts: + whitespace_pattern = '^' + (' ' * min(whitespace_counts)) + content = re.sub(re.compile(whitespace_pattern, re.MULTILINE), '', content) + content = content.strip('\n') + return content + + +def _camelcase_to_spaces(content): + """ + Translate 'CamelCaseNames' to 'Camel Case Names'. + Used when generating names from view classes. + """ + camelcase_boundry = '(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))' + content = re.sub(camelcase_boundry, ' \\1', content).strip() + return ' '.join(content.split('_')).title() + + +def get_view_name(cls): + """ + Return a formatted name for an `APIView` class or `@api_view` function. + """ + name = cls.__name__ + name = _remove_trailing_string(name, 'View') + name = _remove_trailing_string(name, 'ViewSet') + return _camelcase_to_spaces(name) + + +def get_view_description(cls, html=False): + """ + Return a description for an `APIView` class or `@api_view` function. + """ + description = cls.__doc__ or '' + description = _remove_leading_indent(description) + if html: + return markup_description(description) + return description + + +def markup_description(description): + """ + Apply HTML markup to the given description. + """ + if apply_markdown: + description = apply_markdown(description) + else: + description = escape(description).replace('\n', '
    ') + return mark_safe(description) diff --git a/rest_framework/views.py b/rest_framework/views.py index 81cbdcbb2..12298ca52 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -4,51 +4,13 @@ Provides an APIView class that is used as the base of all class-based views. from __future__ import unicode_literals from django.core.exceptions import PermissionDenied from django.http import Http404 -from django.utils.html import escape -from django.utils.safestring import mark_safe from django.views.decorators.csrf import csrf_exempt from rest_framework import status, exceptions -from rest_framework.compat import View, apply_markdown +from rest_framework.compat import View from rest_framework.response import Response from rest_framework.request import Request from rest_framework.settings import api_settings -import re - - -def _remove_trailing_string(content, trailing): - """ - Strip trailing component `trailing` from `content` if it exists. - Used when generating names from view classes. - """ - if content.endswith(trailing) and content != trailing: - return content[:-len(trailing)] - return content - - -def _remove_leading_indent(content): - """ - Remove leading indent from a block of text. - Used when generating descriptions from docstrings. - """ - whitespace_counts = [len(line) - len(line.lstrip(' ')) - for line in content.splitlines()[1:] if line.lstrip()] - - # unindent the content if needed - if whitespace_counts: - whitespace_pattern = '^' + (' ' * min(whitespace_counts)) - content = re.sub(re.compile(whitespace_pattern, re.MULTILINE), '', content) - content = content.strip('\n') - return content - - -def _camelcase_to_spaces(content): - """ - Translate 'CamelCaseNames' to 'Camel Case Names'. - Used when generating names from view classes. - """ - camelcase_boundry = '(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))' - content = re.sub(camelcase_boundry, ' \\1', content).strip() - return ' '.join(content.split('_')).title() +from rest_framework.utils.formatting import get_view_name, get_view_description class APIView(View): @@ -64,13 +26,13 @@ class APIView(View): @classmethod def as_view(cls, **initkwargs): """ - Override the default :meth:`as_view` to store an instance of the view - as an attribute on the callable function. This allows us to discover - information about the view when we do URL reverse lookups. + Store the original class on the view function. + + This allows us to discover information about the view when we do URL + reverse lookups. Used for breadcrumb generation. """ - # TODO: deprecate? view = super(APIView, cls).as_view(**initkwargs) - view.cls_instance = cls(**initkwargs) + view.cls = cls return view @property @@ -90,43 +52,10 @@ class APIView(View): 'Vary': 'Accept' } - def get_name(self): - """ - Return the resource or view class name for use as this view's name. - Override to customize. - """ - # TODO: deprecate? - name = self.__class__.__name__ - name = _remove_trailing_string(name, 'View') - return _camelcase_to_spaces(name) - - def get_description(self, html=False): - """ - Return the resource or view docstring for use as this view's description. - Override to customize. - """ - # TODO: deprecate? - description = self.__doc__ or '' - description = _remove_leading_indent(description) - if html: - return self.markup_description(description) - return description - - def markup_description(self, description): - """ - Apply HTML markup to the description of this view. - """ - # TODO: deprecate? - if apply_markdown: - description = apply_markdown(description) - else: - description = escape(description).replace('\n', '
    ') - return mark_safe(description) - def metadata(self, request): return { - 'name': self.get_name(), - 'description': self.get_description(), + 'name': get_view_name(self.__class__), + 'description': get_view_description(self.__class__), 'renders': [renderer.media_type for renderer in self.renderer_classes], 'parses': [parser.media_type for parser in self.parser_classes], } diff --git a/rest_framework/viewsets.py b/rest_framework/viewsets.py index 887a97223..0818c0d9f 100644 --- a/rest_framework/viewsets.py +++ b/rest_framework/viewsets.py @@ -15,9 +15,10 @@ class ViewSetMixin(object): view = MyViewSet.as_view({'get': 'list', 'post': 'create'}) """ + _is_viewset = True @classonlymethod - def as_view(cls, actions=None, **initkwargs): + def as_view(cls, actions=None, name_suffix=None, **initkwargs): """ Main entry point for a request-response process. @@ -57,6 +58,8 @@ class ViewSetMixin(object): # and possible attributes set by decorators # like csrf_exempt from dispatch update_wrapper(view, cls.dispatch, assigned=()) + + view.cls = cls return view From fd3f538e9f9ef5d4d929c107b9619e0735e426f1 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 4 Apr 2013 21:48:23 +0100 Subject: [PATCH 13/23] Fix up view name/description tests --- rest_framework/tests/description.py | 63 +++++++++++------------------ 1 file changed, 23 insertions(+), 40 deletions(-) diff --git a/rest_framework/tests/description.py b/rest_framework/tests/description.py index 5b3315bcf..52c1a34c1 100644 --- a/rest_framework/tests/description.py +++ b/rest_framework/tests/description.py @@ -4,6 +4,7 @@ from __future__ import unicode_literals from django.test import TestCase from rest_framework.views import APIView from rest_framework.compat import apply_markdown +from rest_framework.utils.formatting import get_view_name, get_view_description # We check that docstrings get nicely un-indented. DESCRIPTION = """an example docstring @@ -49,22 +50,16 @@ MARKED_DOWN_gte_21 = """

    an example docstring

    class TestViewNamesAndDescriptions(TestCase): - def test_resource_name_uses_classname_by_default(self): - """Ensure Resource names are based on the classname by default.""" + def test_view_name_uses_class_name(self): + """ + Ensure view names are based on the class name. + """ class MockView(APIView): pass - self.assertEqual(MockView().get_name(), 'Mock') + self.assertEqual(get_view_name(MockView), 'Mock') - def test_resource_name_can_be_set_explicitly(self): - """Ensure Resource names can be set using the 'get_name' method.""" - example = 'Some Other Name' - class MockView(APIView): - def get_name(self): - return example - self.assertEqual(MockView().get_name(), example) - - def test_resource_description_uses_docstring_by_default(self): - """Ensure Resource names are based on the docstring by default.""" + def test_view_description_uses_docstring(self): + """Ensure view descriptions are based on the docstring.""" class MockView(APIView): """an example docstring ==================== @@ -81,44 +76,32 @@ class TestViewNamesAndDescriptions(TestCase): # hash style header #""" - self.assertEqual(MockView().get_description(), DESCRIPTION) + self.assertEqual(get_view_description(MockView), DESCRIPTION) - def test_resource_description_can_be_set_explicitly(self): - """Ensure Resource descriptions can be set using the 'get_description' method.""" - example = 'Some other description' - - class MockView(APIView): - """docstring""" - def get_description(self): - return example - self.assertEqual(MockView().get_description(), example) - - def test_resource_description_supports_unicode(self): + def test_view_description_supports_unicode(self): + """ + Unicode in docstrings should be respected. + """ class MockView(APIView): """Проверка""" pass - self.assertEqual(MockView().get_description(), "Проверка") + self.assertEqual(get_view_description(MockView), "Проверка") - - def test_resource_description_does_not_require_docstring(self): - """Ensure that empty docstrings do not affect the Resource's description if it has been set using the 'get_description' method.""" - example = 'Some other description' - - class MockView(APIView): - def get_description(self): - return example - self.assertEqual(MockView().get_description(), example) - - def test_resource_description_can_be_empty(self): - """Ensure that if a resource has no doctring or 'description' class attribute, then it's description is the empty string.""" + def test_view_description_can_be_empty(self): + """ + Ensure that if a view has no docstring, + then it's description is the empty string. + """ class MockView(APIView): pass - self.assertEqual(MockView().get_description(), '') + self.assertEqual(get_view_description(MockView), '') def test_markdown(self): - """Ensure markdown to HTML works as expected""" + """ + Ensure markdown to HTML works as expected. + """ if apply_markdown: gte_21_match = apply_markdown(DESCRIPTION) == MARKED_DOWN_gte_21 lt_21_match = apply_markdown(DESCRIPTION) == MARKED_DOWN_lt_21 From 371698331c979305b5684f864ee6bf5b6d11a44e Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 4 Apr 2013 22:24:30 +0100 Subject: [PATCH 14/23] Tweaks --- docs/api-guide/generic-views.md | 4 +-- docs/tutorial/6-viewsets-and-routers.md | 44 +++++++++++++++++-------- rest_framework/generics.py | 9 ++--- rest_framework/mixins.py | 4 +++ rest_framework/routers.py | 12 +++++-- 5 files changed, 49 insertions(+), 24 deletions(-) diff --git a/docs/api-guide/generic-views.md b/docs/api-guide/generic-views.md index 20f1be63a..caf6f53ce 100644 --- a/docs/api-guide/generic-views.md +++ b/docs/api-guide/generic-views.md @@ -208,14 +208,14 @@ Should be mixed in with [SingleObjectAPIView]. Provides a `.update(request, *args, **kwargs)` method, that implements updating and saving an existing model instance. +Also provides a `.partial_update(request, *args, **kwargs)` method, which is similar to the `update` method, except that all fields for the update will be optional. This allows support for HTTP `PATCH` requests. + If an object is updated this returns a `200 OK` response, with a serialized representation of the object as the body of the response. If an object is created, for example when making a `DELETE` request followed by a `PUT` request to the same URL, this returns a `201 Created` response, with a serialized representation of the object as the body of the response. If the request data provided for updating the object was invalid, a `400 Bad Request` response will be returned, with the error details as the body of the response. -A boolean `partial` keyword argument may be supplied to the `.update()` method. If `partial` is set to `True`, all fields for the update will be optional. This allows support for HTTP `PATCH` requests. - Should be mixed in with [SingleObjectAPIView]. ## DestroyModelMixin diff --git a/docs/tutorial/6-viewsets-and-routers.md b/docs/tutorial/6-viewsets-and-routers.md index 8a2108b35..4c1a1abd7 100644 --- a/docs/tutorial/6-viewsets-and-routers.md +++ b/docs/tutorial/6-viewsets-and-routers.md @@ -19,6 +19,8 @@ First of all let's refactor our `UserListView` and `UserDetailView` views into a queryset = User.objects.all() serializer_class = UserSerializer +Here we've used `ReadOnlyModelViewSet` class to automatically provide the default 'read-only' operations. We're still setting the `queryset` and `serializer_class` attributes exactly as we did when we were using regular views, but we no longer need to provide the same information to two seperate classes. + Next we're going to replace the `SnippetList`, `SnippetDetail` and `SnippetHighlight` view classes. We can remove the three views, and again replace them with a single class. from rest_framework import viewsets @@ -29,8 +31,7 @@ Next we're going to replace the `SnippetList`, `SnippetDetail` and `SnippetHighl This viewset automatically provides `list`, `create`, `retrieve`, `update` and `destroy` actions. - Additionally we provide an extra `highlight` action, by using the - `@link` decorator. + Additionally we also provide an extra `highlight` action. """ queryset = Snippet.objects.all() serializer_class = SnippetSerializer @@ -45,25 +46,40 @@ Next we're going to replace the `SnippetList`, `SnippetDetail` and `SnippetHighl def pre_save(self, obj): obj.owner = self.request.user -Notice that we've used the `@link` decorator for the `highlight` method. -This decorator can be used to add custom endpoints, other than the standard `create`/`update`/`delete` endpoints. +This time we've used the `ModelViewSet` class in order to get the complete set of default read and write operations. -The `@link` decorator will +Notice that we've also used the `@link` decorator to create a custom action, named `highlight`. This decorator can be used to add any custom endpoints that don't fit into the standard `create`/`update`/`delete` style. + +Custom actions which use the `@link` decorator will respond to `GET` requests. We could have instead used the `@action` decorator if we wanted an action that responded to `POST` requests. ## Binding ViewSets to URLs explicitly The handler methods only get bound to the actions when we define the URLConf. To see what's going on under the hood let's first explicitly create a set of views from our ViewSets. -In the `urls.py` file we first need to bind our `ViewSet` classes into a set of concrete views. +In the `urls.py` file we bind our `ViewSet` classes into a set of concrete views. from snippets.resources import SnippetResource, UserResource - snippet_list = SnippetViewSet.as_view({'get': 'list', 'post': 'create'}) - snippet_detail = SnippetViewSet.as_view({'get': 'retrieve', 'put': 'update', 'delete': 'destroy'}) - snippet_highlight = SnippetViewSet.as_view({'get': 'highlight'}) - user_list = UserViewSet.as_view({'get': 'list', 'post': 'create'}) - user_detail = UserViewSet.as_view({'get': 'retrieve', 'put': 'update', 'delete': 'destroy'}) + snippet_list = SnippetViewSet.as_view({ + 'get': 'list', + 'post': 'create' + }) + snippet_detail = SnippetViewSet.as_view({ + 'get': 'retrieve', + 'put': 'update', + 'patch': 'partial_update', + 'delete': 'destroy' + }) + snippet_highlight = SnippetViewSet.as_view({ + 'get': 'highlight' + }) + user_list = UserViewSet.as_view({ + 'get': 'list' + }) + user_detail = UserViewSet.as_view({ + 'get': 'retrieve' + }) Notice how we're creating multiple views from each `ViewSet` class, by binding the http methods to the required action for each view. @@ -80,7 +96,7 @@ Now that we've bound our resources into concrete views, that we can register the ## Using Routers -Now that we're using `ViewSet` classes rather than `View` classes, we actually don't need to design the URL conf ourselves. The conventions for wiring up resources into views and urls can be handled automatically, using a `Router` class. All we need to do is register the appropriate view sets with a router, and let it do the rest. +Because we're using `ViewSet` classes rather than `View` classes, we actually don't need to design the URL conf ourselves. The conventions for wiring up resources into views and urls can be handled automatically, using a `Router` class. All we need to do is register the appropriate view sets with a router, and let it do the rest. Here's our re-wired `urls.py` file. @@ -89,14 +105,14 @@ Here's our re-wired `urls.py` file. # Create a router and register our views and view sets with it. router = DefaultRouter() - router.register(r'^/', views.api_root) + router.register(r'^/$', views.api_root) router.register(r'^snippets/', views.SnippetViewSet, 'snippet') router.register(r'^users/', views.UserViewSet, 'user') # The urlconf is determined automatically by the router. urlpatterns = router.urlpatterns - # Add format suffixes to all our URL patterns. + # We can still add format suffixes to all our URL patterns. urlpatterns = format_suffix_patterns(urlpatterns) ## Trade-offs between views vs viewsets. diff --git a/rest_framework/generics.py b/rest_framework/generics.py index 36ecf9150..dea980a56 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -187,8 +187,7 @@ class UpdateAPIView(mixins.UpdateModelMixin, return self.update(request, *args, **kwargs) def patch(self, request, *args, **kwargs): - kwargs['partial'] = True - return self.update(request, *args, **kwargs) + return self.partial_update(request, *args, **kwargs) class ListCreateAPIView(mixins.ListModelMixin, @@ -217,8 +216,7 @@ class RetrieveUpdateAPIView(mixins.RetrieveModelMixin, return self.update(request, *args, **kwargs) def patch(self, request, *args, **kwargs): - kwargs['partial'] = True - return self.update(request, *args, **kwargs) + return self.partial_update(request, *args, **kwargs) class RetrieveDestroyAPIView(mixins.RetrieveModelMixin, @@ -248,8 +246,7 @@ class RetrieveUpdateDestroyAPIView(mixins.RetrieveModelMixin, return self.update(request, *args, **kwargs) def patch(self, request, *args, **kwargs): - kwargs['partial'] = True - return self.update(request, *args, **kwargs) + return self.partial_update(request, *args, **kwargs) def delete(self, request, *args, **kwargs): return self.destroy(request, *args, **kwargs) diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py index 7d9a6e654..c700602e8 100644 --- a/rest_framework/mixins.py +++ b/rest_framework/mixins.py @@ -137,6 +137,10 @@ class UpdateModelMixin(object): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + def partial_update(self, request, *args, **kwargs): + kwargs['partial'] = True + return self.update(request, *args, **kwargs) + def pre_save(self, obj): """ Set any attributes on the object that are implicit in the request. diff --git a/rest_framework/routers.py b/rest_framework/routers.py index c37909ff7..afc51f3bb 100644 --- a/rest_framework/routers.py +++ b/rest_framework/routers.py @@ -20,8 +20,16 @@ class BaseRouter(object): class DefaultRouter(BaseRouter): route_list = [ - (r'$', {'get': 'list', 'post': 'create'}, 'list'), - (r'(?P[^/]+)/$', {'get': 'retrieve', 'put': 'update', 'delete': 'destroy'}, 'detail'), + (r'$', { + 'get': 'list', + 'post': 'create' + }, 'list'), + (r'(?P[^/]+)/$', { + 'get': 'retrieve', + 'put': 'update', + 'patch': 'partial_update', + 'delete': 'destroy' + }, 'detail'), ] extra_routes = r'(?P[^/]+)/%s/$' name_format = '%s-%s' From 027792c981b1442a018e382a6fa2e58496b0b750 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 9 Apr 2013 11:54:51 +0100 Subject: [PATCH 15/23] Viewsets and routers in seperate docs --- docs/api-guide/routers.md | 27 +++++++++++++++++++ .../{viewsets-routers.md => viewsets.md} | 14 +--------- docs/index.md | 8 +++--- docs/template.html | 5 ++-- mkdocs.py | 3 ++- 5 files changed, 38 insertions(+), 19 deletions(-) create mode 100644 docs/api-guide/routers.md rename docs/api-guide/{viewsets-routers.md => viewsets.md} (88%) diff --git a/docs/api-guide/routers.md b/docs/api-guide/routers.md new file mode 100644 index 000000000..dbb352fed --- /dev/null +++ b/docs/api-guide/routers.md @@ -0,0 +1,27 @@ + + +# Routers + +> Resource routing allows you to quickly declare all of the common routes for a given resourceful controller. Instead of declaring separate routes for your index... a resourceful route declares them in a single line of code. +> +> — [Ruby on Rails Documentation][cite] + +Some Web frameworks such as Rails provide functionality for automatically determining how the URLs for an application should be mapped to the logic that deals with handling incoming requests. + +Conversely, Django stops short of automatically generating URLs, and requires you to explicitly manage your URL configuration. + +REST framework adds support for automatic URL routing, which provides you with a simple, quick and consistent way of wiring your view logic to a set of URLs. + +# API Guide + +Routers provide a convenient and simple shortcut for wiring up your application's URLs. + + router = routers.DefaultRouter() + router.register('^/', APIRoot, 'api-root') + router.register('^users/', UserViewSet, 'user') + router.register('^groups/', GroupViewSet, 'group') + router.register('^accounts/', AccountViewSet, 'account') + + urlpatterns = router.urlpatterns + +[cite]: http://guides.rubyonrails.org/routing.html \ No newline at end of file diff --git a/docs/api-guide/viewsets-routers.md b/docs/api-guide/viewsets.md similarity index 88% rename from docs/api-guide/viewsets-routers.md rename to docs/api-guide/viewsets.md index 7813c00d3..83b486dd1 100644 --- a/docs/api-guide/viewsets-routers.md +++ b/docs/api-guide/viewsets.md @@ -1,16 +1,4 @@ - - -# ViewSets & Routers - -> Resource routing allows you to quickly declare all of the common routes for a given resourceful controller. Instead of declaring separate routes for your index... a resourceful route declares them in a single line of code. -> -> — [Ruby on Rails Documentation][cite] - -Some Web frameworks such as Rails provide functionality for automatically determining how the URLs for an application should be mapped to the logic that deals with handling incoming requests. - -Conversely, Django stops short of automatically generating URLs, and requires you to explicitly manage your URL configuration. - -REST framework adds support for automatic URL routing, which provides you with a simple, quick and consistent way of wiring your view logic to a set of URLs. + # ViewSets diff --git a/docs/index.md b/docs/index.md index 469a58852..d51bbe13f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -86,7 +86,7 @@ The tutorial will walk you through the building blocks that make up REST framewo * [3 - Class based views][tut-3] * [4 - Authentication & permissions][tut-4] * [5 - Relationships & hyperlinked APIs][tut-5] -* [6 - ViewSets & Routers][tut-6] +* [6 - Viewsets & routers][tut-6] ## API Guide @@ -96,7 +96,8 @@ The API guide is your complete reference manual to all the functionality provide * [Responses][response] * [Views][views] * [Generic views][generic-views] -* [ViewSets and Routers][viewsets-routers] +* [Viewsets][viewsets] +* [Routers][routers] * [Parsers][parsers] * [Renderers][renderers] * [Serializers][serializers] @@ -205,7 +206,8 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. [response]: api-guide/responses.md [views]: api-guide/views.md [generic-views]: api-guide/generic-views.md -[viewsets-routers]: api-guide/viewsets-routers.md +[viewsets]: api-guide/viewsets.md +[routers]: api-guide/routers.md [parsers]: api-guide/parsers.md [renderers]: api-guide/renderers.md [serializers]: api-guide/serializers.md diff --git a/docs/template.html b/docs/template.html index aec3ecc91..931e51c72 100644 --- a/docs/template.html +++ b/docs/template.html @@ -62,7 +62,7 @@
  • 3 - Class based views
  • 4 - Authentication and permissions
  • 5 - Relationships and hyperlinked APIs
  • -
  • 6 - ViewSets and Routers
  • +
  • 6 - Viewsets and routers
  • Responses
  • Views
  • Generic views
  • -
  • ViewSets and Routers
  • +
  • Viewsets
  • +
  • Routers
  • Parsers
  • Renderers
  • Serializers
  • diff --git a/mkdocs.py b/mkdocs.py index f6cc2b5ae..a13870d10 100755 --- a/mkdocs.py +++ b/mkdocs.py @@ -52,7 +52,8 @@ path_list = [ 'api-guide/responses.md', 'api-guide/views.md', 'api-guide/generic-views.md', - 'api-guide/viewsets-routers.md', + 'api-guide/viewsets.md', + 'api-guide/routers.md', 'api-guide/parsers.md', 'api-guide/renderers.md', 'api-guide/serializers.md', From c73d0e1e39e661c7324eb0df8c3ce6e18f57915b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 9 Apr 2013 18:22:39 +0100 Subject: [PATCH 16/23] Minor cleaning up on View --- rest_framework/compat.py | 20 ++++++++++++-------- rest_framework/views.py | 8 ++++---- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 6551723ab..8bfebe689 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -87,9 +87,7 @@ else: raise ImportError("User model is not to be found.") -# First implementation of Django class-based views did not include head method -# in base View class - https://code.djangoproject.com/ticket/15668 -if django.VERSION >= (1, 4): +if django.VERSION >= (1, 5): from django.views.generic import View else: from django.views.generic import View as _View @@ -97,6 +95,8 @@ else: from django.utils.functional import update_wrapper class View(_View): + # 1.3 does not include head method in base View class + # See: https://code.djangoproject.com/ticket/15668 @classonlymethod def as_view(cls, **initkwargs): """ @@ -126,11 +126,15 @@ else: update_wrapper(view, cls.dispatch, assigned=()) return view -# Taken from @markotibold's attempt at supporting PATCH. -# https://github.com/markotibold/django-rest-framework/tree/patch -http_method_names = set(View.http_method_names) -http_method_names.add('patch') -View.http_method_names = list(http_method_names) # PATCH method is not implemented by Django + # _allowed_methods only present from 1.5 onwards + def _allowed_methods(self): + return [m.upper() for m in self.http_method_names if hasattr(self, m)] + + +# PATCH method is not implemented by Django +if 'patch' not in View.http_method_names: + View.http_method_names = View.http_method_names + ['patch'] + # PUT, DELETE do not require CSRF until 1.4. They should. Make it better. if django.VERSION >= (1, 4): diff --git a/rest_framework/views.py b/rest_framework/views.py index 12298ca52..d7d3a2e2f 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -38,10 +38,9 @@ class APIView(View): @property def allowed_methods(self): """ - Return the list of allowed HTTP methods, uppercased. + Wrap Django's private `_allowed_methods` interface in a public property. """ - return [method.upper() for method in self.http_method_names - if hasattr(self, method)] + return self._allowed_methods() @property def default_response_headers(self): @@ -69,7 +68,8 @@ class APIView(View): def http_method_not_allowed(self, request, *args, **kwargs): """ - Called if `request.method` does not correspond to a handler method. + If `request.method` does not correspond to a handler method, + determine what kind of exception to raise. """ raise exceptions.MethodNotAllowed(request.method) From 099163f81f9d89746de50f3aed2955ead54dba4e Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 9 Apr 2013 18:45:15 +0100 Subject: [PATCH 17/23] Removed SingleObjectMixin and MultipleObjectMixin --- rest_framework/generics.py | 137 ++++++++++++++++++++++++++++--------- rest_framework/mixins.py | 5 +- 2 files changed, 105 insertions(+), 37 deletions(-) diff --git a/rest_framework/generics.py b/rest_framework/generics.py index dea980a56..af3b69dae 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -4,21 +4,35 @@ Generic views that provide commonly needed behaviour. from __future__ import unicode_literals from rest_framework import views, mixins from rest_framework.settings import api_settings -from django.views.generic.detail import SingleObjectMixin -from django.views.generic.list import MultipleObjectMixin - +from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist +from django.core.paginator import Paginator, InvalidPage +from django.http import Http404 +from django.utils.translation import ugettext as _ ### Base classes for the generic views ### + class GenericAPIView(views.APIView): """ Base class for all other generic views. """ - model = None + queryset = None serializer_class = None - model_serializer_class = api_settings.DEFAULT_MODEL_SERIALIZER_CLASS + filter_backend = api_settings.FILTER_BACKEND + paginate_by = api_settings.PAGINATE_BY + paginate_by_param = api_settings.PAGINATE_BY_PARAM + pagination_serializer_class = api_settings.DEFAULT_PAGINATION_SERIALIZER_CLASS + allow_empty = True + page_kwarg = 'page' + + # Pending deprecation + model = None + model_serializer_class = api_settings.DEFAULT_MODEL_SERIALIZER_CLASS + pk_url_kwarg = 'pk' # Not provided in Django 1.3 + slug_url_kwarg = 'slug' # Not provided in Django 1.3 + slug_field = 'slug' def filter_queryset(self, queryset): """ @@ -82,15 +96,7 @@ class GenericAPIView(views.APIView): """ pass - -class MultipleObjectAPIView(MultipleObjectMixin, GenericAPIView): - """ - Base class for generic views onto a queryset. - """ - - paginate_by = api_settings.PAGINATE_BY - paginate_by_param = api_settings.PAGINATE_BY_PARAM - pagination_serializer_class = api_settings.DEFAULT_PAGINATION_SERIALIZER_CLASS + # Pagination def get_pagination_serializer(self, page=None): """ @@ -116,28 +122,81 @@ class MultipleObjectAPIView(MultipleObjectMixin, GenericAPIView): pass return self.paginate_by + def paginate_queryset(self, queryset, page_size, paginator_class=Paginator): + """ + Paginate a queryset. + """ + paginator = paginator_class(queryset, page_size, allow_empty_first_page=self.allow_empty) + page_kwarg = self.page_kwarg + page = self.kwargs.get(page_kwarg) or self.request.GET.get(page_kwarg) or 1 + try: + page_number = int(page) + except ValueError: + if page == 'last': + page_number = paginator.num_pages + else: + raise Http404(_("Page is not 'last', nor can it be converted to an int.")) + try: + page = paginator.page(page_number) + return (paginator, page, page.object_list, page.has_other_pages()) + except InvalidPage as e: + raise Http404(_('Invalid page (%(page_number)s): %(message)s') % { + 'page_number': page_number, + 'message': str(e) + }) -class SingleObjectAPIView(SingleObjectMixin, GenericAPIView): - """ - Base class for generic views onto a model instance. - """ - - pk_url_kwarg = 'pk' # Not provided in Django 1.3 - slug_url_kwarg = 'slug' # Not provided in Django 1.3 - slug_field = 'slug' + def get_queryset(self): + """ + Get the list of items for this view. This must be an iterable, and may + be a queryset (in which qs-specific behavior will be enabled). + """ + if self.queryset is not None: + queryset = self.queryset + if hasattr(queryset, '_clone'): + queryset = queryset._clone() + elif self.model is not None: + queryset = self.model._default_manager.all() + else: + raise ImproperlyConfigured("'%s' must define 'queryset' or 'model'" + % self.__class__.__name__) + return queryset def get_object(self, queryset=None): """ - Override default to add support for object-level permissions. + Returns the object the view is displaying. + By default this requires `self.queryset` and a `pk` or `slug` argument + in the URLconf, but subclasses can override this to return any object. """ - obj = super(SingleObjectAPIView, self).get_object(queryset) + # Use a custom queryset if provided; this is required for subclasses + # like DateDetailView + if queryset is None: + queryset = self.get_queryset() + # Next, try looking up by primary key. + pk = self.kwargs.get(self.pk_url_kwarg, None) + slug = self.kwargs.get(self.slug_url_kwarg, None) + if pk is not None: + queryset = queryset.filter(pk=pk) + # Next, try looking up by slug. + elif slug is not None: + queryset = queryset.filter(**{self.slug_field: slug}) + # If none of those are defined, it's an error. + else: + raise AttributeError("Generic detail view %s must be called with " + "either an object pk or a slug." + % self.__class__.__name__) + try: + # Get the single item from the filtered queryset + obj = queryset.get() + except ObjectDoesNotExist: + raise Http404(_("No %(verbose_name)s found matching the query") % + {'verbose_name': queryset.model._meta.verbose_name}) + self.check_object_permissions(self.request, obj) return obj ### Concrete view classes that provide method handlers ### -### by composing the mixin classes with a base view. ### - +### by composing the mixin classes with the base view. ### class CreateAPIView(mixins.CreateModelMixin, GenericAPIView): @@ -150,7 +209,7 @@ class CreateAPIView(mixins.CreateModelMixin, class ListAPIView(mixins.ListModelMixin, - MultipleObjectAPIView): + GenericAPIView): """ Concrete view for listing a queryset. """ @@ -159,7 +218,7 @@ class ListAPIView(mixins.ListModelMixin, class RetrieveAPIView(mixins.RetrieveModelMixin, - SingleObjectAPIView): + GenericAPIView): """ Concrete view for retrieving a model instance. """ @@ -168,7 +227,7 @@ class RetrieveAPIView(mixins.RetrieveModelMixin, class DestroyAPIView(mixins.DestroyModelMixin, - SingleObjectAPIView): + GenericAPIView): """ Concrete view for deleting a model instance. @@ -178,7 +237,7 @@ class DestroyAPIView(mixins.DestroyModelMixin, class UpdateAPIView(mixins.UpdateModelMixin, - SingleObjectAPIView): + GenericAPIView): """ Concrete view for updating a model instance. @@ -192,7 +251,7 @@ class UpdateAPIView(mixins.UpdateModelMixin, class ListCreateAPIView(mixins.ListModelMixin, mixins.CreateModelMixin, - MultipleObjectAPIView): + GenericAPIView): """ Concrete view for listing a queryset or creating a model instance. """ @@ -205,7 +264,7 @@ class ListCreateAPIView(mixins.ListModelMixin, class RetrieveUpdateAPIView(mixins.RetrieveModelMixin, mixins.UpdateModelMixin, - SingleObjectAPIView): + GenericAPIView): """ Concrete view for retrieving, updating a model instance. """ @@ -221,7 +280,7 @@ class RetrieveUpdateAPIView(mixins.RetrieveModelMixin, class RetrieveDestroyAPIView(mixins.RetrieveModelMixin, mixins.DestroyModelMixin, - SingleObjectAPIView): + GenericAPIView): """ Concrete view for retrieving or deleting a model instance. """ @@ -235,7 +294,7 @@ class RetrieveDestroyAPIView(mixins.RetrieveModelMixin, class RetrieveUpdateDestroyAPIView(mixins.RetrieveModelMixin, mixins.UpdateModelMixin, mixins.DestroyModelMixin, - SingleObjectAPIView): + GenericAPIView): """ Concrete view for retrieving, updating or deleting a model instance. """ @@ -250,3 +309,13 @@ class RetrieveUpdateDestroyAPIView(mixins.RetrieveModelMixin, def delete(self, request, *args, **kwargs): return self.destroy(request, *args, **kwargs) + + +### Deprecated classes ### + +class MultipleObjectAPIView(GenericAPIView): + pass + + +class SingleObjectAPIView(GenericAPIView): + pass diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py index c700602e8..b15cb11fc 100644 --- a/rest_framework/mixins.py +++ b/rest_framework/mixins.py @@ -72,8 +72,7 @@ class ListModelMixin(object): # Default is to allow empty querysets. This can be altered by setting # `.allow_empty = False`, to raise 404 errors on empty querysets. - allow_empty = self.get_allow_empty() - if not allow_empty and not self.object_list: + if not self.allow_empty and not self.object_list: class_name = self.__class__.__name__ error_msg = self.empty_error % {'class_name': class_name} raise Http404(error_msg) @@ -148,7 +147,7 @@ class UpdateModelMixin(object): # pk and/or slug attributes are implicit in the URL. pk = self.kwargs.get(self.pk_url_kwarg, None) slug = self.kwargs.get(self.slug_url_kwarg, None) - slug_field = slug and self.get_slug_field() or None + slug_field = slug and self.slug_field or None if pk: setattr(obj, 'pk', pk) From dc45bc7bfad64a17f3e5ed0f5a487bccc379aac2 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 9 Apr 2013 19:01:01 +0100 Subject: [PATCH 18/23] Add lookup_kwarg --- rest_framework/generics.py | 18 ++++++++++++------ rest_framework/tests/filterset.py | 6 +++--- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/rest_framework/generics.py b/rest_framework/generics.py index af3b69dae..d4a50dcda 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -26,6 +26,7 @@ class GenericAPIView(views.APIView): pagination_serializer_class = api_settings.DEFAULT_PAGINATION_SERIALIZER_CLASS allow_empty = True page_kwarg = 'page' + lookup_kwarg = 'pk' # Pending deprecation model = None @@ -167,23 +168,26 @@ class GenericAPIView(views.APIView): By default this requires `self.queryset` and a `pk` or `slug` argument in the URLconf, but subclasses can override this to return any object. """ - # Use a custom queryset if provided; this is required for subclasses - # like DateDetailView + # Determine the base queryset to use. if queryset is None: queryset = self.get_queryset() - # Next, try looking up by primary key. + + # Perform the lookup filtering. pk = self.kwargs.get(self.pk_url_kwarg, None) slug = self.kwargs.get(self.slug_url_kwarg, None) - if pk is not None: + lookup = self.kwargs.get(self.lookup_kwarg, None) + + if lookup is not None: + queryset = queryset.filter(**{self.lookup_kwarg: lookup}) + elif pk is not None: queryset = queryset.filter(pk=pk) - # Next, try looking up by slug. elif slug is not None: queryset = queryset.filter(**{self.slug_field: slug}) - # If none of those are defined, it's an error. else: raise AttributeError("Generic detail view %s must be called with " "either an object pk or a slug." % self.__class__.__name__) + try: # Get the single item from the filtered queryset obj = queryset.get() @@ -191,7 +195,9 @@ class GenericAPIView(views.APIView): raise Http404(_("No %(verbose_name)s found matching the query") % {'verbose_name': queryset.model._meta.verbose_name}) + # May raise a permission denied self.check_object_permissions(self.request, obj) + return obj diff --git a/rest_framework/tests/filterset.py b/rest_framework/tests/filterset.py index 1a71558c0..1e53a5cdb 100644 --- a/rest_framework/tests/filterset.py +++ b/rest_framework/tests/filterset.py @@ -61,7 +61,7 @@ if django_filters: class CommonFilteringTestCase(TestCase): def _serialize_object(self, obj): return {'id': obj.id, 'text': obj.text, 'decimal': obj.decimal, 'date': obj.date} - + def setUp(self): """ Create 10 FilterableItem instances. @@ -190,7 +190,7 @@ class IntegrationTestDetailFiltering(CommonFilteringTestCase): Integration tests for filtered detail views. """ urls = 'rest_framework.tests.filterset' - + def _get_url(self, item): return reverse('detail-view', kwargs=dict(pk=item.pk)) @@ -221,7 +221,7 @@ class IntegrationTestDetailFiltering(CommonFilteringTestCase): response = self.client.get('{url}?decimal={param}'.format(url=self._get_url(low_item), param=search_decimal)) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data, low_item_data) - + # Tests that multiple filters works. search_decimal = Decimal('5.25') search_date = datetime.date(2012, 10, 2) From 1de6cff11b71e4aaa7b76219d4d2118021e23a00 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 9 Apr 2013 19:06:49 +0100 Subject: [PATCH 19/23] Cleaning up get_object and get_queryset --- rest_framework/generics.py | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/rest_framework/generics.py b/rest_framework/generics.py index d4a50dcda..4ae2ac8ea 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -148,25 +148,22 @@ class GenericAPIView(views.APIView): def get_queryset(self): """ - Get the list of items for this view. This must be an iterable, and may - be a queryset (in which qs-specific behavior will be enabled). + Get the list of items for this view. + + This must be an iterable, and may be a queryset. """ if self.queryset is not None: - queryset = self.queryset - if hasattr(queryset, '_clone'): - queryset = queryset._clone() - elif self.model is not None: - queryset = self.model._default_manager.all() - else: - raise ImproperlyConfigured("'%s' must define 'queryset' or 'model'" - % self.__class__.__name__) - return queryset + return self.queryset._clone() + + if self.model is not None: + return self.model._default_manager.all() + + raise ImproperlyConfigured("'%s' must define 'queryset' or 'model'" + % self.__class__.__name__) def get_object(self, queryset=None): """ Returns the object the view is displaying. - By default this requires `self.queryset` and a `pk` or `slug` argument - in the URLconf, but subclasses can override this to return any object. """ # Determine the base queryset to use. if queryset is None: From 9bb1277e512a88e6c11c52457d0c24e73f30bb98 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 9 Apr 2013 19:37:19 +0100 Subject: [PATCH 20/23] Cleaning up around bits of API that will be pending deprecation --- rest_framework/generics.py | 116 ++++++++++++++++++++++--------------- rest_framework/mixins.py | 9 +-- rest_framework/viewsets.py | 6 +- 3 files changed, 75 insertions(+), 56 deletions(-) diff --git a/rest_framework/generics.py b/rest_framework/generics.py index 4ae2ac8ea..124dba38d 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -35,15 +35,6 @@ class GenericAPIView(views.APIView): slug_url_kwarg = 'slug' # Not provided in Django 1.3 slug_field = 'slug' - def filter_queryset(self, queryset): - """ - Given a queryset, filter it with whichever filter backend is in use. - """ - if not self.filter_backend: - return queryset - backend = self.filter_backend() - return backend.filter_queryset(self.request, queryset, self) - def get_serializer_context(self): """ Extra context provided to the serializer class. @@ -54,24 +45,6 @@ class GenericAPIView(views.APIView): 'view': self } - def get_serializer_class(self): - """ - Return the class to use for the serializer. - - Defaults to using `self.serializer_class`, falls back to constructing a - model serializer class using `self.model_serializer_class`, with - `self.model` as the model. - """ - serializer_class = self.serializer_class - - if serializer_class is None: - class DefaultSerializer(self.model_serializer_class): - class Meta: - model = self.model - serializer_class = DefaultSerializer - - return serializer_class - def get_serializer(self, instance=None, data=None, files=None, many=False, partial=False): """ @@ -83,22 +56,6 @@ class GenericAPIView(views.APIView): return serializer_class(instance, data=data, files=files, many=many, partial=partial, context=context) - def pre_save(self, obj): - """ - Placeholder method for calling before saving an object. - May be used eg. to set attributes on the object that are implicit - in either the request, or the url. - """ - pass - - def post_save(self, obj, created=False): - """ - Placeholder method for calling after saving an object. - """ - pass - - # Pagination - def get_pagination_serializer(self, page=None): """ Return a serializer instance to use with paginated data. @@ -111,9 +68,14 @@ class GenericAPIView(views.APIView): context = self.get_serializer_context() return pagination_serializer_class(instance=page, context=context) - def get_paginate_by(self, queryset): + def get_paginate_by(self, queryset=None): """ Return the size of pages to use with pagination. + + If `PAGINATE_BY_PARAM` is set it will attempt to get the page size + from a named query parameter in the url, eg. ?page_size=100 + + Otherwise defaults to using `self.paginate_by`. """ if self.paginate_by_param: query_params = self.request.QUERY_PARAMS @@ -121,6 +83,7 @@ class GenericAPIView(views.APIView): return int(query_params[self.paginate_by_param]) except (KeyError, ValueError): pass + return self.paginate_by def paginate_queryset(self, queryset, page_size, paginator_class=Paginator): @@ -146,16 +109,54 @@ class GenericAPIView(views.APIView): 'message': str(e) }) + def filter_queryset(self, queryset): + """ + Given a queryset, filter it with whichever filter backend is in use. + """ + if not self.filter_backend: + return queryset + backend = self.filter_backend() + return backend.filter_queryset(self.request, queryset, self) + + ### The following methods provide default implementations + ### that you may want to override for more complex cases. + + def get_serializer_class(self): + """ + Return the class to use for the serializer. + Defaults to using `self.serializer_class`. + + You may want to override this if you need to provide different + serializations depending on the incoming request. + + (Eg. admins get full serialization, others get basic serilization) + """ + serializer_class = self.serializer_class + if serializer_class is not None: + return serializer_class + + # TODO: Deprecation warning + class DefaultSerializer(self.model_serializer_class): + class Meta: + model = self.model + return DefaultSerializer + def get_queryset(self): """ Get the list of items for this view. - This must be an iterable, and may be a queryset. + Defaults to using `self.queryset`. + + You may want to override this if you need to provide different + querysets depending on the incoming request. + + (Eg. return a list of items that is specific to the user) """ if self.queryset is not None: return self.queryset._clone() if self.model is not None: + # TODO: Deprecation warning return self.model._default_manager.all() raise ImproperlyConfigured("'%s' must define 'queryset' or 'model'" @@ -164,10 +165,14 @@ class GenericAPIView(views.APIView): def get_object(self, queryset=None): """ Returns the object the view is displaying. + + You may want to override this if you need to provide non-standard + queryset lookups. Eg if objects are referenced using multiple + keyword arguments in the url conf. """ # Determine the base queryset to use. if queryset is None: - queryset = self.get_queryset() + queryset = self.filter_queryset(self.get_queryset()) # Perform the lookup filtering. pk = self.kwargs.get(self.pk_url_kwarg, None) @@ -177,8 +182,10 @@ class GenericAPIView(views.APIView): if lookup is not None: queryset = queryset.filter(**{self.lookup_kwarg: lookup}) elif pk is not None: + # TODO: Deprecation warning queryset = queryset.filter(pk=pk) elif slug is not None: + # TODO: Deprecation warning queryset = queryset.filter(**{self.slug_field: slug}) else: raise AttributeError("Generic detail view %s must be called with " @@ -197,6 +204,23 @@ class GenericAPIView(views.APIView): return obj + ### The following methods are intended to be overridden. + + def pre_save(self, obj): + """ + Placeholder method for calling before saving an object. + + May be used to set attributes on the object that are implicit + in either the request, or the url. + """ + pass + + def post_save(self, obj, created=False): + """ + Placeholder method for calling after saving an object. + """ + pass + ### Concrete view classes that provide method handlers ### ### by composing the mixin classes with the base view. ### diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py index b15cb11fc..6e40b5c46 100644 --- a/rest_framework/mixins.py +++ b/rest_framework/mixins.py @@ -67,8 +67,7 @@ class ListModelMixin(object): empty_error = "Empty list and '%(class_name)s.allow_empty' is False." def list(self, request, *args, **kwargs): - queryset = self.get_queryset() - self.object_list = self.filter_queryset(queryset) + self.object_list = self.filter_queryset(self.get_queryset()) # Default is to allow empty querysets. This can be altered by setting # `.allow_empty = False`, to raise 404 errors on empty querysets. @@ -79,7 +78,7 @@ class ListModelMixin(object): # Pagination size is set by the `.paginate_by` attribute, # which may be `None` to disable pagination. - page_size = self.get_paginate_by(self.object_list) + page_size = self.get_paginate_by() if page_size: packed = self.paginate_queryset(self.object_list, page_size) paginator, page, queryset, is_paginated = packed @@ -96,9 +95,7 @@ class RetrieveModelMixin(object): Should be mixed in with `SingleObjectAPIView`. """ def retrieve(self, request, *args, **kwargs): - queryset = self.get_queryset() - filtered_queryset = self.filter_queryset(queryset) - self.object = self.get_object(filtered_queryset) + self.object = self.get_object() serializer = self.get_serializer(self.object) return Response(serializer.data) diff --git a/rest_framework/viewsets.py b/rest_framework/viewsets.py index 0818c0d9f..28ab30e2f 100644 --- a/rest_framework/viewsets.py +++ b/rest_framework/viewsets.py @@ -76,14 +76,12 @@ class ModelViewSet(mixins.CreateModelMixin, mixins.DestroyModelMixin, mixins.ListModelMixin, ViewSetMixin, - generics.MultipleObjectAPIView, - generics.SingleObjectAPIView): + generics.GenericAPIView): pass class ReadOnlyModelViewSet(mixins.RetrieveModelMixin, mixins.ListModelMixin, ViewSetMixin, - generics.MultipleObjectAPIView, - generics.SingleObjectAPIView): + generics.GenericAPIView): pass From 07af4373616c28e7600ee2ec7981b5a1d0a92f7d Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 9 Apr 2013 19:47:16 +0100 Subject: [PATCH 21/23] Cleaning up around bits of API that will be pending deprecation --- rest_framework/generics.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/rest_framework/generics.py b/rest_framework/generics.py index 124dba38d..ba7d1f432 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -24,15 +24,17 @@ class GenericAPIView(views.APIView): paginate_by = api_settings.PAGINATE_BY paginate_by_param = api_settings.PAGINATE_BY_PARAM pagination_serializer_class = api_settings.DEFAULT_PAGINATION_SERIALIZER_CLASS - allow_empty = True page_kwarg = 'page' lookup_kwarg = 'pk' + allow_empty = True + + ###################################### + # These are all pending deprecation... - # Pending deprecation model = None model_serializer_class = api_settings.DEFAULT_MODEL_SERIALIZER_CLASS - pk_url_kwarg = 'pk' # Not provided in Django 1.3 - slug_url_kwarg = 'slug' # Not provided in Django 1.3 + pk_url_kwarg = 'pk' + slug_url_kwarg = 'slug' slug_field = 'slug' def get_serializer_context(self): @@ -90,7 +92,8 @@ class GenericAPIView(views.APIView): """ Paginate a queryset. """ - paginator = paginator_class(queryset, page_size, allow_empty_first_page=self.allow_empty) + paginator = paginator_class(queryset, page_size, + allow_empty_first_page=self.allow_empty) page_kwarg = self.page_kwarg page = self.kwargs.get(page_kwarg) or self.request.GET.get(page_kwarg) or 1 try: @@ -118,6 +121,7 @@ class GenericAPIView(views.APIView): backend = self.filter_backend() return backend.filter_queryset(self.request, queryset, self) + ######################## ### The following methods provide default implementations ### that you may want to override for more complex cases. @@ -204,7 +208,9 @@ class GenericAPIView(views.APIView): return obj - ### The following methods are intended to be overridden. + ######################## + ### The following are placeholder methods, + ### and are intended to be overridden. def pre_save(self, obj): """ @@ -222,8 +228,10 @@ class GenericAPIView(views.APIView): pass +########################################################## ### Concrete view classes that provide method handlers ### ### by composing the mixin classes with the base view. ### +########################################################## class CreateAPIView(mixins.CreateModelMixin, GenericAPIView): @@ -338,7 +346,9 @@ class RetrieveUpdateDestroyAPIView(mixins.RetrieveModelMixin, return self.destroy(request, *args, **kwargs) +########################## ### Deprecated classes ### +########################## class MultipleObjectAPIView(GenericAPIView): pass From e0020c5b033308cd789408a8823d6707deed8032 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 11 Apr 2013 15:48:18 +0100 Subject: [PATCH 22/23] Simplify get_object --- rest_framework/generics.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/rest_framework/generics.py b/rest_framework/generics.py index ba7d1f432..ea62123d0 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -4,9 +4,10 @@ Generic views that provide commonly needed behaviour. from __future__ import unicode_literals from rest_framework import views, mixins from rest_framework.settings import api_settings -from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist +from django.core.exceptions import ImproperlyConfigured from django.core.paginator import Paginator, InvalidPage from django.http import Http404 +from django.shortcuts import get_object_or_404 from django.utils.translation import ugettext as _ ### Base classes for the generic views ### @@ -163,7 +164,7 @@ class GenericAPIView(views.APIView): # TODO: Deprecation warning return self.model._default_manager.all() - raise ImproperlyConfigured("'%s' must define 'queryset' or 'model'" + raise ImproperlyConfigured("'%s' must define 'queryset'" % self.__class__.__name__) def get_object(self, queryset=None): @@ -177,6 +178,8 @@ class GenericAPIView(views.APIView): # Determine the base queryset to use. if queryset is None: queryset = self.filter_queryset(self.get_queryset()) + else: + pass # Deprecation warning # Perform the lookup filtering. pk = self.kwargs.get(self.pk_url_kwarg, None) @@ -184,24 +187,19 @@ class GenericAPIView(views.APIView): lookup = self.kwargs.get(self.lookup_kwarg, None) if lookup is not None: - queryset = queryset.filter(**{self.lookup_kwarg: lookup}) + filter_kwargs = {self.lookup_kwarg: lookup} elif pk is not None: # TODO: Deprecation warning - queryset = queryset.filter(pk=pk) + filter_kwargs = {'pk': pk} elif slug is not None: # TODO: Deprecation warning - queryset = queryset.filter(**{self.slug_field: slug}) + filter_kwargs = {self.slug_field: slug} else: raise AttributeError("Generic detail view %s must be called with " "either an object pk or a slug." % self.__class__.__name__) - try: - # Get the single item from the filtered queryset - obj = queryset.get() - except ObjectDoesNotExist: - raise Http404(_("No %(verbose_name)s found matching the query") % - {'verbose_name': queryset.model._meta.verbose_name}) + obj = get_object_or_404(queryset, **filter_kwargs) # May raise a permission denied self.check_object_permissions(self.request, obj) From d75cebf75696602170a9d282d4b114d01d6e5d8e Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 11 Apr 2013 15:48:41 +0100 Subject: [PATCH 23/23] Remove router bit from viewset docs --- docs/api-guide/viewsets.md | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/docs/api-guide/viewsets.md b/docs/api-guide/viewsets.md index 83b486dd1..cf6ae33b6 100644 --- a/docs/api-guide/viewsets.md +++ b/docs/api-guide/viewsets.md @@ -2,6 +2,11 @@ # ViewSets +> After routing has determined which controller to use for a request, your controller is responsible for making sense of the request and producing the appropriate output. +> +> — [Ruby on Rails Documentation][cite] + + Django REST framework allows you to combine the logic for a set of related views in a single class, called a `ViewSet`. In other frameworks you may also find conceptually similar implementations named something like 'Resources' or 'Controllers'. A `ViewSet` class is simply **a type of class-based View, that does not provide any method handlers** such as `.get()` or `.post()`, and instead provides actions such as `.list()` and `.create()`. @@ -128,18 +133,4 @@ By creating your own base `ViewSet` classes, you can provide common behavior tha 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. ---- - -# Routers - -Routers provide a convenient and simple shortcut for wiring up your application's URLs. - - router = routers.DefaultRouter() - router.register('^/', APIRoot, 'api-root') - router.register('^users/', UserViewSet, 'user') - router.register('^groups/', GroupViewSet, 'group') - router.register('^accounts/', AccountViewSet, 'account') - - urlpatterns = router.urlpatterns - [cite]: http://guides.rubyonrails.org/routing.html \ No newline at end of file