diff --git a/docs/api-guide/viewsets-routers.md b/docs/api-guide/viewsets-routers.md
new file mode 100644
index 000000000..7813c00d3
--- /dev/null
+++ b/docs/api-guide/viewsets-routers.md
@@ -0,0 +1,157 @@
+
+
+# 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.
+
+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.
+
+---
+
+# 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
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
@@ -71,6 +72,7 @@
Responses
Views
Generic views
+ ViewSets and 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..8a2108b35
--- /dev/null
+++ b/docs/tutorial/6-viewsets-and-routers.md
@@ -0,0 +1,107 @@
+# 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
+
+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,
+ 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` method.
+This decorator can be used to add custom endpoints, other than the standard `create`/`update`/`delete` endpoints.
+
+The `@link` decorator will
+
+## 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.
+
+ 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'})
+
+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
+
+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 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
+
+ # 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..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/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/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..c37909ff7
--- /dev/null
+++ b/rest_framework/routers.py
@@ -0,0 +1,73 @@
+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', '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/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
new file mode 100644
index 000000000..0818c0d9f
--- /dev/null
+++ b/rest_framework/viewsets.py
@@ -0,0 +1,89 @@
+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.MultipleObjectAPIView,
+ generics.SingleObjectAPIView):
+ pass
+
+
+class ReadOnlyModelViewSet(mixins.RetrieveModelMixin,
+ mixins.ListModelMixin,
+ ViewSetMixin,
+ generics.MultipleObjectAPIView,
+ generics.SingleObjectAPIView):
+ pass