Commit 835d3f89 by Tom Christie

Merge remove-django-generics

parents 4bf1a09b d75cebf7
...@@ -208,14 +208,14 @@ Should be mixed in with [SingleObjectAPIView]. ...@@ -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. 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 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 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. 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]. Should be mixed in with [SingleObjectAPIView].
## DestroyModelMixin ## DestroyModelMixin
......
<a class="github" href="routers.py"></a>
# 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.
>
> &mdash; [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
<a class="github" href="viewsets.py"></a>
# 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.
>
> &mdash; [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
...@@ -86,6 +86,7 @@ The tutorial will walk you through the building blocks that make up REST framewo ...@@ -86,6 +86,7 @@ The tutorial will walk you through the building blocks that make up REST framewo
* [3 - Class based views][tut-3] * [3 - Class based views][tut-3]
* [4 - Authentication & permissions][tut-4] * [4 - Authentication & permissions][tut-4]
* [5 - Relationships & hyperlinked APIs][tut-5] * [5 - Relationships & hyperlinked APIs][tut-5]
* [6 - Viewsets & routers][tut-6]
## API Guide ## API Guide
...@@ -95,6 +96,8 @@ The API guide is your complete reference manual to all the functionality provide ...@@ -95,6 +96,8 @@ The API guide is your complete reference manual to all the functionality provide
* [Responses][response] * [Responses][response]
* [Views][views] * [Views][views]
* [Generic views][generic-views] * [Generic views][generic-views]
* [Viewsets][viewsets]
* [Routers][routers]
* [Parsers][parsers] * [Parsers][parsers]
* [Renderers][renderers] * [Renderers][renderers]
* [Serializers][serializers] * [Serializers][serializers]
...@@ -197,11 +200,14 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ...@@ -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-3]: tutorial/3-class-based-views.md
[tut-4]: tutorial/4-authentication-and-permissions.md [tut-4]: tutorial/4-authentication-and-permissions.md
[tut-5]: tutorial/5-relationships-and-hyperlinked-apis.md [tut-5]: tutorial/5-relationships-and-hyperlinked-apis.md
[tut-6]: tutorial/6-viewsets-and-routers.md
[request]: api-guide/requests.md [request]: api-guide/requests.md
[response]: api-guide/responses.md [response]: api-guide/responses.md
[views]: api-guide/views.md [views]: api-guide/views.md
[generic-views]: api-guide/generic-views.md [generic-views]: api-guide/generic-views.md
[viewsets]: api-guide/viewsets.md
[routers]: api-guide/routers.md
[parsers]: api-guide/parsers.md [parsers]: api-guide/parsers.md
[renderers]: api-guide/renderers.md [renderers]: api-guide/renderers.md
[serializers]: api-guide/serializers.md [serializers]: api-guide/serializers.md
......
...@@ -62,6 +62,7 @@ ...@@ -62,6 +62,7 @@
<li><a href="{{ base_url }}/tutorial/3-class-based-views{{ suffix }}">3 - Class based views</a></li> <li><a href="{{ base_url }}/tutorial/3-class-based-views{{ suffix }}">3 - Class based views</a></li>
<li><a href="{{ base_url }}/tutorial/4-authentication-and-permissions{{ suffix }}">4 - Authentication and permissions</a></li> <li><a href="{{ base_url }}/tutorial/4-authentication-and-permissions{{ suffix }}">4 - Authentication and permissions</a></li>
<li><a href="{{ base_url }}/tutorial/5-relationships-and-hyperlinked-apis{{ suffix }}">5 - Relationships and hyperlinked APIs</a></li> <li><a href="{{ base_url }}/tutorial/5-relationships-and-hyperlinked-apis{{ suffix }}">5 - Relationships and hyperlinked APIs</a></li>
<li><a href="{{ base_url }}/tutorial/6-viewsets-and-routers{{ suffix }}">6 - Viewsets and routers</a></li>
</ul> </ul>
</li> </li>
<li class="dropdown"> <li class="dropdown">
...@@ -71,6 +72,8 @@ ...@@ -71,6 +72,8 @@
<li><a href="{{ base_url }}/api-guide/responses{{ suffix }}">Responses</a></li> <li><a href="{{ base_url }}/api-guide/responses{{ suffix }}">Responses</a></li>
<li><a href="{{ base_url }}/api-guide/views{{ suffix }}">Views</a></li> <li><a href="{{ base_url }}/api-guide/views{{ suffix }}">Views</a></li>
<li><a href="{{ base_url }}/api-guide/generic-views{{ suffix }}">Generic views</a></li> <li><a href="{{ base_url }}/api-guide/generic-views{{ suffix }}">Generic views</a></li>
<li><a href="{{ base_url }}/api-guide/viewsets{{ suffix }}">Viewsets</a></li>
<li><a href="{{ base_url }}/api-guide/routers{{ suffix }}">Routers</a></li>
<li><a href="{{ base_url }}/api-guide/parsers{{ suffix }}">Parsers</a></li> <li><a href="{{ base_url }}/api-guide/parsers{{ suffix }}">Parsers</a></li>
<li><a href="{{ base_url }}/api-guide/renderers{{ suffix }}">Renderers</a></li> <li><a href="{{ base_url }}/api-guide/renderers{{ suffix }}">Renderers</a></li>
<li><a href="{{ base_url }}/api-guide/serializers{{ suffix }}">Serializers</a></li> <li><a href="{{ base_url }}/api-guide/serializers{{ suffix }}">Serializers</a></li>
......
# 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<pk>[0-9]+)/$', snippet_detail, name='snippet-detail'),
url(r'^snippets/(?P<pk>[0-9]+)/highlight/$', snippet_highlight, name='snippet-highlight'),
url(r'^users/$', user_list, name='user-list'),
url(r'^users/(?P<pk>[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.
...@@ -47,10 +47,13 @@ path_list = [ ...@@ -47,10 +47,13 @@ path_list = [
'tutorial/3-class-based-views.md', 'tutorial/3-class-based-views.md',
'tutorial/4-authentication-and-permissions.md', 'tutorial/4-authentication-and-permissions.md',
'tutorial/5-relationships-and-hyperlinked-apis.md', 'tutorial/5-relationships-and-hyperlinked-apis.md',
'tutorial/6-viewsets-and-routers.md',
'api-guide/requests.md', 'api-guide/requests.md',
'api-guide/responses.md', 'api-guide/responses.md',
'api-guide/views.md', 'api-guide/views.md',
'api-guide/generic-views.md', 'api-guide/generic-views.md',
'api-guide/viewsets.md',
'api-guide/routers.md',
'api-guide/parsers.md', 'api-guide/parsers.md',
'api-guide/renderers.md', 'api-guide/renderers.md',
'api-guide/serializers.md', 'api-guide/serializers.md',
......
...@@ -87,9 +87,7 @@ else: ...@@ -87,9 +87,7 @@ else:
raise ImportError("User model is not to be found.") raise ImportError("User model is not to be found.")
# First implementation of Django class-based views did not include head method if django.VERSION >= (1, 5):
# in base View class - https://code.djangoproject.com/ticket/15668
if django.VERSION >= (1, 4):
from django.views.generic import View from django.views.generic import View
else: else:
from django.views.generic import View as _View from django.views.generic import View as _View
...@@ -97,6 +95,8 @@ else: ...@@ -97,6 +95,8 @@ else:
from django.utils.functional import update_wrapper from django.utils.functional import update_wrapper
class View(_View): class View(_View):
# 1.3 does not include head method in base View class
# See: https://code.djangoproject.com/ticket/15668
@classonlymethod @classonlymethod
def as_view(cls, **initkwargs): def as_view(cls, **initkwargs):
""" """
...@@ -126,11 +126,15 @@ else: ...@@ -126,11 +126,15 @@ else:
update_wrapper(view, cls.dispatch, assigned=()) update_wrapper(view, cls.dispatch, assigned=())
return view return view
# Taken from @markotibold's attempt at supporting PATCH. # _allowed_methods only present from 1.5 onwards
# https://github.com/markotibold/django-rest-framework/tree/patch def _allowed_methods(self):
http_method_names = set(View.http_method_names) return [m.upper() for m in self.http_method_names if hasattr(self, m)]
http_method_names.add('patch')
View.http_method_names = list(http_method_names) # PATCH method is not implemented by Django
# 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. # PUT, DELETE do not require CSRF until 1.4. They should. Make it better.
if django.VERSION >= (1, 4): if django.VERSION >= (1, 4):
......
...@@ -97,3 +97,25 @@ def permission_classes(permission_classes): ...@@ -97,3 +97,25 @@ def permission_classes(permission_classes):
func.permission_classes = permission_classes func.permission_classes = permission_classes
return func return func
return decorator 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
...@@ -67,20 +67,18 @@ class ListModelMixin(object): ...@@ -67,20 +67,18 @@ class ListModelMixin(object):
empty_error = "Empty list and '%(class_name)s.allow_empty' is False." empty_error = "Empty list and '%(class_name)s.allow_empty' is False."
def list(self, request, *args, **kwargs): def list(self, request, *args, **kwargs):
queryset = self.get_queryset() self.object_list = self.filter_queryset(self.get_queryset())
self.object_list = self.filter_queryset(queryset)
# Default is to allow empty querysets. This can be altered by setting # Default is to allow empty querysets. This can be altered by setting
# `.allow_empty = False`, to raise 404 errors on empty querysets. # `.allow_empty = False`, to raise 404 errors on empty querysets.
allow_empty = self.get_allow_empty() if not self.allow_empty and not self.object_list:
if not allow_empty and not self.object_list:
class_name = self.__class__.__name__ class_name = self.__class__.__name__
error_msg = self.empty_error % {'class_name': class_name} error_msg = self.empty_error % {'class_name': class_name}
raise Http404(error_msg) raise Http404(error_msg)
# Pagination size is set by the `.paginate_by` attribute, # Pagination size is set by the `.paginate_by` attribute,
# which may be `None` to disable pagination. # 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: if page_size:
packed = self.paginate_queryset(self.object_list, page_size) packed = self.paginate_queryset(self.object_list, page_size)
paginator, page, queryset, is_paginated = packed paginator, page, queryset, is_paginated = packed
...@@ -135,6 +133,10 @@ class UpdateModelMixin(object): ...@@ -135,6 +133,10 @@ class UpdateModelMixin(object):
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) 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): def pre_save(self, obj):
""" """
Set any attributes on the object that are implicit in the request. Set any attributes on the object that are implicit in the request.
...@@ -142,7 +144,7 @@ class UpdateModelMixin(object): ...@@ -142,7 +144,7 @@ class UpdateModelMixin(object):
# pk and/or slug attributes are implicit in the URL. # pk and/or slug attributes are implicit in the URL.
pk = self.kwargs.get(self.pk_url_kwarg, None) pk = self.kwargs.get(self.pk_url_kwarg, None)
slug = self.kwargs.get(self.slug_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: if pk:
setattr(obj, 'pk', pk) setattr(obj, 'pk', pk)
......
...@@ -24,6 +24,7 @@ from rest_framework.settings import api_settings ...@@ -24,6 +24,7 @@ from rest_framework.settings import api_settings
from rest_framework.request import clone_request from rest_framework.request import clone_request
from rest_framework.utils import encoders from rest_framework.utils import encoders
from rest_framework.utils.breadcrumbs import get_breadcrumbs 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 from rest_framework import exceptions, parsers, status, VERSION
...@@ -438,16 +439,10 @@ class BrowsableAPIRenderer(BaseRenderer): ...@@ -438,16 +439,10 @@ class BrowsableAPIRenderer(BaseRenderer):
return GenericContentForm() return GenericContentForm()
def get_name(self, view): def get_name(self, view):
try: return get_view_name(view.__class__)
return view.get_name()
except AttributeError:
return smart_text(view.__class__.__name__)
def get_description(self, view): def get_description(self, view):
try: return get_view_description(view.__class__, html=True)
return view.get_description(html=True)
except AttributeError:
return smart_text(view.__doc__ or '')
def render(self, data, accepted_media_type=None, renderer_context=None): def render(self, data, accepted_media_type=None, renderer_context=None):
""" """
......
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<pk>[^/]+)/$', {
'get': 'retrieve',
'put': 'update',
'patch': 'partial_update',
'delete': 'destroy'
}, 'detail'),
]
extra_routes = r'(?P<pk>[^/]+)/%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
...@@ -4,6 +4,7 @@ from __future__ import unicode_literals ...@@ -4,6 +4,7 @@ from __future__ import unicode_literals
from django.test import TestCase from django.test import TestCase
from rest_framework.views import APIView from rest_framework.views import APIView
from rest_framework.compat import apply_markdown 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. # We check that docstrings get nicely un-indented.
DESCRIPTION = """an example docstring DESCRIPTION = """an example docstring
...@@ -49,22 +50,16 @@ MARKED_DOWN_gte_21 = """<h2 id="an-example-docstring">an example docstring</h2> ...@@ -49,22 +50,16 @@ MARKED_DOWN_gte_21 = """<h2 id="an-example-docstring">an example docstring</h2>
class TestViewNamesAndDescriptions(TestCase): class TestViewNamesAndDescriptions(TestCase):
def test_resource_name_uses_classname_by_default(self): def test_view_name_uses_class_name(self):
"""Ensure Resource names are based on the classname by default.""" """
Ensure view names are based on the class name.
"""
class MockView(APIView): class MockView(APIView):
pass pass
self.assertEqual(MockView().get_name(), 'Mock') self.assertEqual(get_view_name(MockView), 'Mock')
def test_resource_name_can_be_set_explicitly(self): def test_view_description_uses_docstring(self):
"""Ensure Resource names can be set using the 'get_name' method.""" """Ensure view descriptions are based on the docstring."""
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."""
class MockView(APIView): class MockView(APIView):
"""an example docstring """an example docstring
==================== ====================
...@@ -81,44 +76,32 @@ class TestViewNamesAndDescriptions(TestCase): ...@@ -81,44 +76,32 @@ class TestViewNamesAndDescriptions(TestCase):
# hash style header #""" # 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): class MockView(APIView):
"""Проверка""" """Проверка"""
pass 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): def test_view_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.""" """
Ensure that if a view has no docstring,
then it's description is the empty string.
"""
class MockView(APIView): class MockView(APIView):
pass pass
self.assertEqual(MockView().get_description(), '') self.assertEqual(get_view_description(MockView), '')
def test_markdown(self): def test_markdown(self):
"""Ensure markdown to HTML works as expected""" """
Ensure markdown to HTML works as expected.
"""
if apply_markdown: if apply_markdown:
gte_21_match = apply_markdown(DESCRIPTION) == MARKED_DOWN_gte_21 gte_21_match = apply_markdown(DESCRIPTION) == MARKED_DOWN_gte_21
lt_21_match = apply_markdown(DESCRIPTION) == MARKED_DOWN_lt_21 lt_21_match = apply_markdown(DESCRIPTION) == MARKED_DOWN_lt_21
......
...@@ -61,7 +61,7 @@ if django_filters: ...@@ -61,7 +61,7 @@ if django_filters:
class CommonFilteringTestCase(TestCase): class CommonFilteringTestCase(TestCase):
def _serialize_object(self, obj): def _serialize_object(self, obj):
return {'id': obj.id, 'text': obj.text, 'decimal': obj.decimal, 'date': obj.date} return {'id': obj.id, 'text': obj.text, 'decimal': obj.decimal, 'date': obj.date}
def setUp(self): def setUp(self):
""" """
Create 10 FilterableItem instances. Create 10 FilterableItem instances.
...@@ -190,7 +190,7 @@ class IntegrationTestDetailFiltering(CommonFilteringTestCase): ...@@ -190,7 +190,7 @@ class IntegrationTestDetailFiltering(CommonFilteringTestCase):
Integration tests for filtered detail views. Integration tests for filtered detail views.
""" """
urls = 'rest_framework.tests.filterset' urls = 'rest_framework.tests.filterset'
def _get_url(self, item): def _get_url(self, item):
return reverse('detail-view', kwargs=dict(pk=item.pk)) return reverse('detail-view', kwargs=dict(pk=item.pk))
...@@ -221,7 +221,7 @@ class IntegrationTestDetailFiltering(CommonFilteringTestCase): ...@@ -221,7 +221,7 @@ class IntegrationTestDetailFiltering(CommonFilteringTestCase):
response = self.client.get('{url}?decimal={param}'.format(url=self._get_url(low_item), param=search_decimal)) 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.status_code, status.HTTP_200_OK)
self.assertEqual(response.data, low_item_data) self.assertEqual(response.data, low_item_data)
# Tests that multiple filters works. # Tests that multiple filters works.
search_decimal = Decimal('5.25') search_decimal = Decimal('5.25')
search_date = datetime.date(2012, 10, 2) search_date = datetime.date(2012, 10, 2)
......
from __future__ import unicode_literals from __future__ import unicode_literals
from django.core.urlresolvers import resolve, get_script_prefix from django.core.urlresolvers import resolve, get_script_prefix
from rest_framework.utils.formatting import get_view_name
def get_breadcrumbs(url): def get_breadcrumbs(url):
...@@ -16,11 +17,11 @@ def get_breadcrumbs(url): ...@@ -16,11 +17,11 @@ def get_breadcrumbs(url):
pass pass
else: else:
# Check if this is a REST framework view, and if so add it to the breadcrumbs # 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. # Don't list the same view twice in a row.
# Probably an optional trailing slash. # Probably an optional trailing slash.
if not seen or seen[-1] != view: 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) seen.append(view)
if url == '': if url == '':
......
"""
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', '<br />')
return mark_safe(description)
...@@ -4,51 +4,13 @@ Provides an APIView class that is used as the base of all class-based views. ...@@ -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 __future__ import unicode_literals
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.http import Http404, HttpResponse 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 django.views.decorators.csrf import csrf_exempt
from rest_framework import status, exceptions 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.response import Response
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.settings import api_settings from rest_framework.settings import api_settings
import re from rest_framework.utils.formatting import get_view_name, get_view_description
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()
class APIView(View): class APIView(View):
...@@ -64,22 +26,21 @@ class APIView(View): ...@@ -64,22 +26,21 @@ class APIView(View):
@classmethod @classmethod
def as_view(cls, **initkwargs): def as_view(cls, **initkwargs):
""" """
Override the default :meth:`as_view` to store an instance of the view Store the original class on the view function.
as an attribute on the callable function. This allows us to discover
information about the view when we do URL reverse lookups. 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 = super(APIView, cls).as_view(**initkwargs)
view.cls_instance = cls(**initkwargs) view.cls = cls
return view return view
@property @property
def allowed_methods(self): 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 return self._allowed_methods()
if hasattr(self, method)]
@property @property
def default_response_headers(self): def default_response_headers(self):
...@@ -90,43 +51,10 @@ class APIView(View): ...@@ -90,43 +51,10 @@ class APIView(View):
'Vary': 'Accept' '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', '<br />')
return mark_safe(description)
def metadata(self, request): def metadata(self, request):
return { return {
'name': self.get_name(), 'name': get_view_name(self.__class__),
'description': self.get_description(), 'description': get_view_description(self.__class__),
'renders': [renderer.media_type for renderer in self.renderer_classes], 'renders': [renderer.media_type for renderer in self.renderer_classes],
'parses': [parser.media_type for parser in self.parser_classes], 'parses': [parser.media_type for parser in self.parser_classes],
} }
...@@ -140,7 +68,8 @@ class APIView(View): ...@@ -140,7 +68,8 @@ class APIView(View):
def http_method_not_allowed(self, request, *args, **kwargs): 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) raise exceptions.MethodNotAllowed(request.method)
......
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
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment