diff --git a/docs/api-guide/generic-views.md b/docs/api-guide/generic-views.md
index cef9a9d44..c73bc700b 100755
--- 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/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.md b/docs/api-guide/viewsets.md
new file mode 100644
index 000000000..cf6ae33b6
--- /dev/null
+++ b/docs/api-guide/viewsets.md
@@ -0,0 +1,136 @@
+
+
+# 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()`.
+
+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.
+
+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
+
+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()`.
+
+#### 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.
+
+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.
+
+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.
+
+[cite]: http://guides.rubyonrails.org/routing.html
\ No newline at end of file
diff --git a/docs/index.md b/docs/index.md
index 4c2720c89..d51bbe13f 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,8 @@ The API guide is your complete reference manual to all the functionality provide
* [Responses][response]
* [Views][views]
* [Generic views][generic-views]
+* [Viewsets][viewsets]
+* [Routers][routers]
* [Parsers][parsers]
* [Renderers][renderers]
* [Serializers][serializers]
@@ -197,11 +200,14 @@ 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]: 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 7e9297627..931e51c72 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
@@ -71,6 +72,8 @@
Responses
Views
Generic views
+ Viewsets
+ Routers
Parsers
Renderers
Serializers
diff --git a/docs/tutorial/6-viewsets-and-routers.md b/docs/tutorial/6-viewsets-and-routers.md
new file mode 100644
index 000000000..4c1a1abd7
--- /dev/null
+++ b/docs/tutorial/6-viewsets-and-routers.md
@@ -0,0 +1,123 @@
+# Tutorial 6 - ViewSets & Routers
+
+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.
+
+`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`.
+
+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 ViewSets
+
+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.ReadOnlyModelViewSet):
+ """
+ This viewset automatically provides `list` and `detail` actions.
+ """
+ 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
+ from rest_framework.decorators import link
+
+ class SnippetViewSet(viewsets.ModelViewSet):
+ """
+ This viewset automatically provides `list`, `create`, `retrieve`,
+ `update` and `destroy` actions.
+
+ Additionally we also provide an extra `highlight` action.
+ """
+ queryset = Snippet.objects.all()
+ 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
+
+This time we've used the `ModelViewSet` class in order to get the complete set of default read and write operations.
+
+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 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',
+ '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.
+
+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')
+ ))
+
+## Using Routers
+
+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.
+
+ 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'^/$', 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
+
+ # We can still add format suffixes to all our URL patterns.
+ urlpatterns = format_suffix_patterns(urlpatterns)
+
+## Trade-offs between views vs viewsets.
+
+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 instead of function based views. Using view sets is less explicit than building your views individually.
+
diff --git a/mkdocs.py b/mkdocs.py
index dadb17d27..a13870d10 100755
--- a/mkdocs.py
+++ b/mkdocs.py
@@ -47,10 +47,13 @@ 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.md',
+ 'api-guide/routers.md',
'api-guide/parsers.md',
'api-guide/renderers.md',
'api-guide/serializers.md',
diff --git a/rest_framework/compat.py b/rest_framework/compat.py
index 067e90183..3828555b9 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/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/generics.py b/rest_framework/generics.py
index f9133c735..ae03060b9 100644
--- a/rest_framework/generics.py
+++ b/rest_framework/generics.py
@@ -4,30 +4,41 @@ 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
+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 ###
+
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
- 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)
+ # Shortcut which may be used in place of `queryset`/`serializer_class`
+ model = None
+
+ 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
+ model_serializer_class = api_settings.DEFAULT_MODEL_SERIALIZER_CLASS
+ page_kwarg = 'page'
+ lookup_field = 'pk'
+ allow_empty = True
+
+ ######################################
+ # These are pending deprecation...
+
+ pk_url_kwarg = 'pk'
+ slug_url_kwarg = 'slug'
+ slug_field = 'slug'
def get_serializer_context(self):
"""
@@ -39,24 +50,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):
"""
@@ -68,30 +61,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
-
-
-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
-
def get_pagination_serializer(self, page=None):
"""
Return a serializer instance to use with paginated data.
@@ -104,9 +73,14 @@ class MultipleObjectAPIView(MultipleObjectMixin, GenericAPIView):
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
@@ -114,31 +88,155 @@ class MultipleObjectAPIView(MultipleObjectMixin, GenericAPIView):
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):
+ """
+ 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.
- """
+ def filter_queryset(self, queryset):
+ """
+ Given a queryset, filter it with whichever filter backend is in use.
- pk_url_kwarg = 'pk' # Not provided in Django 1.3
- slug_url_kwarg = 'slug' # Not provided in Django 1.3
- slug_field = 'slug'
+ You are unlikely to want to override this method, although you may need
+ to call it either from a list view, or from a custom `get_object`
+ method if you want to apply the configured filtering backend to the
+ default queryset.
+ """
+ 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'"
+ % self.__class__.__name__)
def get_object(self, queryset=None):
"""
- Override default to add support for object-level permissions.
+ 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.
"""
- queryset = self.filter_queryset(self.get_queryset())
- obj = super(SingleObjectAPIView, self).get_object(queryset)
+ # 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)
+ slug = self.kwargs.get(self.slug_url_kwarg, None)
+ lookup = self.kwargs.get(self.lookup_field, None)
+
+ if lookup is not None:
+ filter_kwargs = {self.lookup_field: lookup}
+ elif pk is not None:
+ # TODO: Deprecation warning
+ filter_kwargs = {'pk': pk}
+ elif slug is not None:
+ # TODO: Deprecation warning
+ 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__)
+
+ obj = get_object_or_404(queryset, **filter_kwargs)
+
+ # May raise a permission denied
self.check_object_permissions(self.request, obj)
+
return obj
+ ########################
+ ### The following are placeholder methods,
+ ### and 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 a base view. ###
-
+### by composing the mixin classes with the base view. ###
+##########################################################
class CreateAPIView(mixins.CreateModelMixin,
GenericAPIView):
@@ -151,7 +249,7 @@ class CreateAPIView(mixins.CreateModelMixin,
class ListAPIView(mixins.ListModelMixin,
- MultipleObjectAPIView):
+ GenericAPIView):
"""
Concrete view for listing a queryset.
"""
@@ -160,7 +258,7 @@ class ListAPIView(mixins.ListModelMixin,
class RetrieveAPIView(mixins.RetrieveModelMixin,
- SingleObjectAPIView):
+ GenericAPIView):
"""
Concrete view for retrieving a model instance.
"""
@@ -169,7 +267,7 @@ class RetrieveAPIView(mixins.RetrieveModelMixin,
class DestroyAPIView(mixins.DestroyModelMixin,
- SingleObjectAPIView):
+ GenericAPIView):
"""
Concrete view for deleting a model instance.
@@ -179,7 +277,7 @@ class DestroyAPIView(mixins.DestroyModelMixin,
class UpdateAPIView(mixins.UpdateModelMixin,
- SingleObjectAPIView):
+ GenericAPIView):
"""
Concrete view for updating a model instance.
@@ -188,13 +286,12 @@ 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,
mixins.CreateModelMixin,
- MultipleObjectAPIView):
+ GenericAPIView):
"""
Concrete view for listing a queryset or creating a model instance.
"""
@@ -207,7 +304,7 @@ class ListCreateAPIView(mixins.ListModelMixin,
class RetrieveUpdateAPIView(mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
- SingleObjectAPIView):
+ GenericAPIView):
"""
Concrete view for retrieving, updating a model instance.
"""
@@ -218,13 +315,12 @@ 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,
mixins.DestroyModelMixin,
- SingleObjectAPIView):
+ GenericAPIView):
"""
Concrete view for retrieving or deleting a model instance.
"""
@@ -238,7 +334,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.
"""
@@ -249,8 +345,19 @@ 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)
+
+
+##########################
+### Deprecated classes ###
+##########################
+
+class MultipleObjectAPIView(GenericAPIView):
+ pass
+
+
+class SingleObjectAPIView(GenericAPIView):
+ pass
diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py
index 3bd7d6dfc..6e40b5c46 100644
--- a/rest_framework/mixins.py
+++ b/rest_framework/mixins.py
@@ -67,20 +67,18 @@ 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.
- 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)
# 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
@@ -135,6 +133,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.
@@ -142,7 +144,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)
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
new file mode 100644
index 000000000..afc51f3bb
--- /dev/null
+++ b/rest_framework/routers.py
@@ -0,0 +1,81 @@
+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'):
+ self._urlpatterns = patterns('', *self.get_urlpatterns())
+ return self._urlpatterns
+
+
+class DefaultRouter(BaseRouter):
+ route_list = [
+ (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'
+
+ 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, action_name 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
+
+ # Build the url pattern
+ regex = prefix + suffix
+ 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 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
+
+ suffix = self.extra_routes % action_name
+
+ # Build the url pattern
+ 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
+ return ret
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
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)
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 7c97607b7..b8e948e09 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, HttpResponse
-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,22 +26,21 @@ 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
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):
@@ -90,43 +51,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],
}
@@ -140,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)
diff --git a/rest_framework/viewsets.py b/rest_framework/viewsets.py
new file mode 100644
index 000000000..28ab30e2f
--- /dev/null
+++ b/rest_framework/viewsets.py
@@ -0,0 +1,87 @@
+from functools import update_wrapper
+from django.utils.decorators import classonlymethod
+from rest_framework import views, generics, mixins
+
+
+class ViewSetMixin(object):
+ """
+ This is the magic.
+
+ Overrides `.as_view()` so that it takes an `actions` keyword that performs
+ the binding of HTTP methods to actions on the Resource.
+
+ For example, to create a concrete view binding the 'GET' and 'POST' methods
+ to the 'list' and 'create' actions...
+
+ view = MyViewSet.as_view({'get': 'list', 'post': 'create'})
+ """
+ _is_viewset = True
+
+ @classonlymethod
+ def as_view(cls, actions=None, name_suffix=None, **initkwargs):
+ """
+ Main entry point for a request-response process.
+
+ Because of the way class based views create a closure around the
+ instantiated view, we need to totally reimplement `.as_view`,
+ and slightly modify the view function that is created and returned.
+ """
+ # sanitize keyword arguments
+ for key in initkwargs:
+ if key in cls.http_method_names:
+ raise TypeError("You tried to pass in the %s method name as a "
+ "keyword argument to %s(). Don't do that."
+ % (key, cls.__name__))
+ if not hasattr(cls, key):
+ raise TypeError("%s() received an invalid keyword %r" % (
+ cls.__name__, key))
+
+ def view(request, *args, **kwargs):
+ self = cls(**initkwargs)
+
+ # Bind methods to actions
+ # This is the bit that's different to a standard view
+ for method, action in actions.items():
+ handler = getattr(self, action)
+ setattr(self, method, handler)
+
+ # Patch this in as it's otherwise only present from 1.5 onwards
+ if hasattr(self, 'get') and not hasattr(self, 'head'):
+ self.head = self.get
+
+ # And continue as usual
+ return self.dispatch(request, *args, **kwargs)
+
+ # take name and docstring from class
+ update_wrapper(view, cls, updated=())
+
+ # and possible attributes set by decorators
+ # like csrf_exempt from dispatch
+ update_wrapper(view, cls.dispatch, assigned=())
+
+ view.cls = cls
+ 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.GenericAPIView):
+ pass
+
+
+class ReadOnlyModelViewSet(mixins.RetrieveModelMixin,
+ mixins.ListModelMixin,
+ ViewSetMixin,
+ generics.GenericAPIView):
+ pass